Spring Security默认是基于session进行用户认证的,用户通过登录申请实现认证之后,认证信息在服务器端保留在session中,之后的申请发送上来后SecurityContextPersistenceFilter过滤器从session中获取认证信息、以便通过后续平安过滤器的安全检查。
明天的指标是替换Spring Security默认的session保留认证信息的机制为通过JWT的形式进行认证。
JWT(JSON WEB TOKEN)的相干内容就不做详细分析了,咱们只须要晓得以下几点:
- 用户登录认证(用户名、明码验证)通过之后,系统生成token并送给前端。
- token中蕴含用户id(或用户名)以及过期工夫,蕴含通过加密机制生成的摘要,具备防篡改的能力。
- token信息不须要在服务器端保留,前端获取到token之后,每次申请都必须携带该token。
- 后盾接管到申请之后,查看没有token、或者token验证不通过则不生成认证信息,否则,token验证通过则示意该用户通过认证。
- 后盾接管到的token如果已过期,则依据利用的需要自动更新token或者要求前端从新登录。
与session计划比照一下,咱们须要解决的问题如下:
- 须要停用掉Spring Security默认的session治理用户认证信息的计划。
- 用户登录后须要生成并返回给前端token。
- 前端申请上来之后,须要获取并验证token,验证通过后生成用户认证信息。
上面咱们逐个解决上述三个问题。咱们依然应用上一篇文章中用过的demo,曾经贴出过的代码就不再贴出了。
筹备工作
咱们须要筹备一些与JWT相干的货色,比方引入JWT的生成token、token验证的模块。
咱们引入java-jwt,在pom文件退出依赖即可:
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.2.1</version> </dependency>
而后须要编写一个工具类,以便可能生成、验证token,咱们临时不思考token过期等等细节问题的解决,只有能正确生成、验证token就能够:
public class JwtUtil { public final static String SECRET_KEY="This is secret key for JWT"; public final static String JWTHeader_Leading_Str="Bearer "; public final static String JWTHeader_Name="Authorization"; public static String generateToken(String userName){ Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND,120); HashMap header = new HashMap<>(); header.put("alg","HS256"); header.put("Type","JWT"); return JWT.create().withHeader(header) .withClaim("userName",userName) .withExpiresAt(calendar.getTime()) .sign(Algorithm.HMAC256(SECRET_KEY)); } public static String verify(String token){ // 创立解析对象,应用的算法和secret要与创立token时保持一致 JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build(); // 解析指定的token DecodedJWT decodedJWT = jwtVerifier.verify(token); return decodedJWT.getClaims().get("userName").asString(); } public static String parseToken(HttpServletRequest request){ String rawJwt = request.getHeader(JWTHeader_Name); if(rawJwt==null){ return null; } if(!rawJwt.startsWith(JWTHeader_Leading_Str)){ return null; } return rawJwt.substring(JWTHeader_Leading_Str.length()+1); } private void showToken(DecodedJWT decodedJWT){ // 获取解析后的token中的信息 String header = decodedJWT.getHeader(); System.out.println("type:" + decodedJWT.getType()); System.out.println("header:" + header); Map<String, Claim> payloadMap = decodedJWT.getClaims(); System.out.println("Payload:" + payloadMap); Date expires = decodedJWT.getExpiresAt(); System.out.println("过期工夫:" + expires); String signature = decodedJWT.getSignature(); System.out.println("signature:" + signature); } public static void main(String[] args) { String token=JwtUtil.generateToken("Zhang Fu"); System.out.println(token); String userName = JwtUtil.verify(token); System.out.println("userName:" +userName); }}
OK,筹备工作实现。
停用Spring Security的默认session计划
为了停用session,咱们须要减少一项配置,所以咱们要新建一个配置文件:
@Configurationpublic class WebSecurityConfig { @Autowired MyRememberMeService myRememberMeService; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() //.antMatchers("/hello").permitAll() .anyRequest().authenticated().and() .httpBasic().and() .rememberMe().rememberMeServices(myRememberMeService) .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .formLogin(); //httpSecurity.addFilterBefore(new JwtSecurityFilter(), UsernamePasswordAuthenticationFilter.class); //httpSecurity.addFilterAfter(new JwtAfterUsernamePasswordFilter(),UsernamePasswordAuthenticationFilter.class); return httpSecurity.build(); }}
设置SessionCreationPolicy.STATELESS就能够达到目标。
起因能够在sessionManagementConfigure.java这个session配置器中找到,在他的init办法中:
@Override public void init(H http) { SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); boolean stateless = isStateless(); if (securityContextRepository == null) { if (stateless) { http.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository()); }
如果SessionCreationPolicy设置为stateless的话,那么他会创立NullSecurityContextRepository作为他的SecurityContextRepository。
这个NullSecurityContextRepository理论就是个假把式,啥也不干,咱们晓得用户认证通过后会调用他的saveContext办法存储认证信息,他是这么干的:
@Override public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { }
所以,他就是个偷工减料的货,啥也没干。
所以第一个问题解决了。
用户登录后生成token并返回给前端
这个问题我尝试了好几个计划之后才胜利。
咱们晓得用户登录是在平安过滤器UsernamePasswordAuthenticationFilter中实现的,登录胜利后如果想要生成JWT的token,计划无非就是:
- UsernamePasswordAuthenticationFilter之后加一个咱们本人的过滤器,与UsernamePasswordAuthenticationFilter一样只匹配登录申请,生成token。
- UsernamePasswordAuthenticationFilter过滤器认证通过后有没有调用过其余能够被咱们客户化的东东,咱们客户化这个东东实现咱们的指标。
- 客户化UsernamePasswordAuthenticationFilter,登录胜利后生成token。
这里必须交代一下,第3个计划只是从逻辑上来说应该能解决咱们的问题,然而压根就没有思考过这个计划,因为我感觉太麻烦。
先试了第一个计划,没胜利,因为咱们晓得Spring Security还有一个RequestCacheAwareFilter过滤器,会导致如果你是在尚未获取受权之前拜访了非登录页面,那么Spring Security会导航到登录页面、登录胜利后在UsernamePasswordAuthenticationFilter中就会产生跳转,这样的话就跳过了咱们前面加的这个过滤器,指标就无奈实现或者说即便弯弯绕绕能实现,然而计划也不会太好。
所以,就致力钻研第2个计划。
所以大略看了一下UsernamePasswordAuthenticationFilter在登录认证胜利后的解决,发现了这个:
所以就大略去钻研了一下RememberMeServices,读了一下他的doc,发现他是一个基于cokie的、确保前台申请即便在session过期之后发送上来都能够持续通过平安认证的“记住我”机制。
除了cokie之外,其余的与JWT的要求齐全吻合。如果咱们能本人实现一个基于JWT的RememberMeService,是不是就解决问题了?
所以就用他来尝试一下。
MyRememberMeService#loginSuccess
创立MyRememberMeService并通过配置退出到利用中来,后面停用session的时候的配置文件中曾经退出了,返回去看一眼就行。
咱们要实现他的loginSuccess办法,创立并返回token:
@Override public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = successfulAuthentication.getName(); String token= JwtUtil.generateToken(username); token=JwtUtil.JWTHeader_Leading_Str+token; log.info("After login success:"+token); response.setHeader(JwtUtil.JWTHeader_Name,token); }
验证一下创立并返回token
启动我的项目,胜利登录零碎后,惊喜的发现他曾经开始干活了:
好了,给了咱们信念,撸起袖子加油干!
RememberMeAuthenticationFilter
RememberMeServices机制依赖RememberMeAuthenticationFilter实现,咱们在下面的配置文件中曾经启用了。
而后简略看一眼RememberMeAuthenticationFilter过滤器的doFilter办法,他首先去SecurityContextHolder获取认证信息,如果没有获取到的话,就调用RememberMeService的autoLogin办法,只是从doFilter的源码来看(代码就不贴出了),autoLogin办法返回的Authentication并未实现认证,因为返回之后还要调用authenticationManager进行认证。
这是与咱们预期不符的中央,咱们心愿autoLogin之后就能够实现认证、并且能够将认证信息搁置到SecurityContextHolder中(因为咱们是通过JWT做验证的,token验证通过的话就相当于实现了认证)。
那咱们是不是能够在autoLogin中实现这些操作,并且返回null骗一下RememberMeAuthenticationFilter的doFilter办法不再要求authenticationManager去再次认证呢?
咱们试一下!
MyRememberMeService#autoLogin
创立RememberMeService并实现autoLogin办法,为了简化他的初始化过程,咱们间接把他注入到Spring Ioc容器中。
如前所述,办法肯定要返回null。
@Slf4j@Componentpublic class MyRememberMeService implements RememberMeServices { @Autowired MyUserDetailsService myUserDetailsService; @Override public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { log.info("autoLogin in MyRememberMeService: "); String username; String token = JwtUtil.parseToken(request); if(token==null){ log.info("I dont get token from header"); token=request.getParameter("token"); } log.info("finally the token is :" + token); UsernamePasswordAuthenticationToken authenticationToken=null; if(token!=null) { String userName=JwtUtil.verify(token); UserDetails user = myUserDetailsService.loadUserByUsername(userName); authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } return null; } @Override public void loginFail(HttpServletRequest request, HttpServletResponse response) { }}
测试一下autoLogin
下面的代码中曾经看到了,咱们只是为了测试、如果从申请头信息中拿不到token的话就从申请参数中获取。只是为了学习、测试偷个懒,正式我的项目实现的时候这个中央还是须要比拟多的欠缺的。
启动我的项目,开始测试,第一步先通过login获取token,下面曾经展现过了,而后用获取到的token发一个须要认证的申请,token加在申请参数前面:
如图,申请胜利了!
上一篇Spring Security自定义用户认证过程(2)