当前位置: 首页 > 后端技术 > Java

SpringSecurity+JWT

时间:2023-04-01 23:07:55 Java

SpringSecurity默认是基于session的用户认证。用户通过登录请求完成认证后,认证信息保存在服务器端的session中。发送后续请求后,SecurityContextPersistenceFilter过滤器从会话中获取身份验证。信息,以便通过后续安全过滤器的安全检查。今天的目标是将SpringSecurity默认保存认证信息的session机制替换为JWT认证。JWT(JSONWEBTOKEN)的相关内容就不详细分析了。我们只需要知道以下几点:用户登录认证(用户名、密码认证)通过后,系统生成一个token发送给前端。令牌包含用户id(或用户名)和过期时间,并包含通过加密机制生成的摘要,是不可篡改的。令牌信息不需要保存在服务端。前端获取token后,每次请求都要携带token。后台收到请求后检查没有token,如果token校验不通过,则不会生成认证信息;否则,如果令牌验证通过,则表示用户已通过身份验证。如果后台收到的token已经过期,则根据应用的需要自动更新token或者要求前端重新登录。与session方案相比,我们需要解决的问题如下:需要禁用SpringSecurity默认的session管理用户认证信息的方案。用户登录后,需要生成token返回给前端。前端请求上来后,需要获取并验证token,验证通过后生成用户认证信息。下面我们一一解决以上三个问题。我们还是用上篇文章用的demo,已经贴出来的代码就不贴了。准备工作我们需要准备一些JWT相关的东西,比如引入JWT生成token,token验证模块。我们引入java-jwt,在pom文件中添加依赖:com.auth0java-jwt4.2.1然后你需要写一个工具类来生成和验证令牌。token过期等细节的处理我们暂时不考虑,只要token能够正确生成和验证即可:publicfinalstaticStringJWTHeader_Leading_Str="Bearer";publicfinalstaticStringJWTHeader_Name="Authorization";publicstaticStringgenerateToken(StringuserName){Calendarcalendar=Calendar.getInstance();calendar.add(日历.SECOND,120);HashMapheader=newHashMap<>();header.put("alg","HS256");header.put("类型","JWT");返回JWT.create().withHeader(header).withClaim("userName",userName).withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SECRET_KEY));}publicstaticStringverify(Stringtoken){//创建分析对象,使用的算法和secret要和创建token一致JWTVerifierjwtVerifier=JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();//解析指定tokenDecodedJWTdecodedJWT=jwtVerifier.verify(token);返回decodedJWT.getClaims().get("userName").asString();}publicstaticStringparseToken(HttpServletRequestrequest){StringrawJwt=request.getHeader(JWTHeader_Name);如果(rawJwt==null){返回空值;}if(!rawJwt.startsWith(JWTHeader_Leading_Str)){返回空;}返回rawJwt.substring(JWTHeader_Leading_Str.length()+1);}privatevoidshowToken(DecodedJWTdecodedJWT){//获取解析后的token中的信息Stringheader=decodedJWT.getHeader();System.out.println("类型:"+decodedJWT.getType());System.out.println("表头:"+header);MappayloadMap=decodedJWT.getClaims();System.out.println("载荷:"+payloadMap);日期到期=decodedJWT.getExpiresAt();System.out.println("过期时间:"+expires);字符串签名=decodedJWT.getSignature();System.out.println("签名:"+signature);}publicstaticvoidmain(String[]args){Stringtoken=JwtUtil.generateToken("张付");System.out.println(令牌);StringuserName=JwtUtil.verify(token);System.out.println("用户名:"+userName);}}OK,准备工作完成禁用SpringSecurity的默认session方案为了禁用session,我们需要添加一个配置,所以我们需要新建一个配置文件:@ConfigurationpublicclassWebSecurityConfig@BeanpublicSecurityFilterChainsecurityFilterChain(HttpSecurityhttpSecurity)throwsException{httpSecurity.authorizeRequests()//.antMatchers("/hello").permitAll().anyRequest().authenticated().and().httpBasic().and().rememberMe().rememberMeServices(myRememberMeService).and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().formLogin();//httpSecurity.addFilterBefore(newJwtSecurityFilter(),UsernamePasswordAuthenticationFilter.class);//httpSecurity.addFilterAfter(newJwtAfterUsernamePasswordFilterAssword(),User.class);返回httpSecurity.build();}}设置会话CreationPolicy.STATELESS可以达到目的。原因可以在sessionManagementConfigure.java的会话配置器中找到,在它的init方法中:@Overridepublicvoidinit(Hhttp){布尔无状态=isStateless();if(securityContextRepository==null){if(stateless){http.setSharedObject(SecurityContextRepository.class,newNullSecurityContextRepository());如果SessionCreationPolicy设置为无状态,那么他将创建NullSecurityContextRepository作为他的SecurityContextRepository。这个NullSecurityContextRepository实际上是一个假句柄,什么都不做。我们知道,用户通过认证后,会调用他的saveContext方法存储认证信息。他是这样做的:@OverridepublicvoidsaveContext(SecurityContextcontext,HttpServletRequestrequest,HttpServletResponseresponse){}所以,他只是偷工减料,什么也没做。这样第一个问题就解决了。用户登录后生成token返回给前端。在成功之前,我尝试了几种解决方案。我们知道用户登录是在安全过滤器UsernamePasswordAuthenticationFilter中完成的。如果想在登录成功后生成JWTtoken,解决办法无非就是在UsernamePasswordAuthenticationFilter后面加一个我们自己的filter,和UsernamePasswordAuthenticationFilter一样只匹配登录请求生成token。UsernamePasswordAuthenticationFilter是否在通过过滤器认证后调用了其他我们可以自定义的东西?我们定制这个东西来完成我们的目标。自定义UsernamePasswordAuthenticationFilter,在登录成功后生成token。这里必须说明一下,第三种方案在逻辑上应该可以解决我们的问题,但是我从来没有考虑过这种方案,因为我觉得太麻烦了。我先尝试了第一种解决方案,但是没有成功,因为我们知道SpringSecurity还有一个RequestCacheAwareFilter过滤器,这会导致如果在没有获得授权之前访问非登录页面,那么SpringSecurity会导航到登录页面并记录日志在成功。最后在UsernamePasswordAuthenticationFilter中会发生跳转,这样我们后面添加的filter就会被跳过,达到不了目的,或者即使能达到也不好解决。因此,尝试研究第二个选项。于是我看了一下UsernamePasswordAuthenticationFilter在登录认证成功后的处理,发现是这样的:所以我大概研究了RememberMeServices,看了他的doc,发现是基于cookie的,即使session过期也能保证前台请求发送上去后可以继续通过安全认证的“记住我”机制。除了cookie,其他的完全符合JWT的要求。如果我们自己实现一个基于JWT的RememberMeService,是不是就可以解决问题呢?所以就和他一起试试吧。MyRememberMeService#loginSuccess创建MyRememberMeService,通过配置将其添加到应用中。之前禁用session的时候已经添加到配置文件中了,回去看看就好了。我们要实现他的loginSuccess方法,创建并返回令牌:@OverridepublicvoidloginSuccess(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationsuccessfulAuthentication){Stringusername=successfulAuthentication.getName();Stringtoken=JwtUtil(生成名称)JwtUtil.JWTHeader_Leading_Str+token;log.info("登录成功后:"+token);response.setHeader(JwtUtil.JWTHeader_Name,token);}验证创建并返回令牌启动项目。成功登录系统后,惊讶地发现已经开始工作了:嗯,给我们信心,撸起袖子加油干吧!RememberMeAuthenticationFilterRememberMeServices机制依赖于我们在上述配置文件中启用的RememberMeAuthenticationFilter实现。然后简单看一下RememberMeAuthenticationFilter过滤器的doFilter方法。它首先转到SecurityContextHolder以获取身份验证信息。如果未获取,则调用RememberMeService的autoLogin方法。只看doFilter的源码(代码就不贴出来了),autoLogin方法返回的Authentication并没有完成认证,因为返回后需要调用authenticationManager进行认证。这不符合我们的预期。我们希望autoLogin后可以完成鉴权,鉴权信息可以放在SecurityContextHolder中(因为我们是通过JWT验证的,token验证就相当于完成了鉴权)。那么我们是否可以在autoLogin中完成这些操作,返回null来欺骗RememberMeAuthenticationFilter的doFilter方法,不再需要authenticationManager重新认证呢?让我们试试吧!MyRememberMeService#autoLogin创建RememberMeService并实现autoLogin方法。为了简化它的初始化过程,我们直接将它注入到SpringIoc容器中。如前所述,该方法必须返回null。@Slf4j@ComponentpublicclassMyRememberMeServiceimplementsRememberMeServices{@AutowiredMyUserDetailsS??ervicemyUserDetailsS??ervice;@OverridepublicAuthenticationautoLogin(HttpServletRequest请求,HttpServletResponse响应){log.info(“MyRememberMeService中的自动登录:”);字符串用户名;Stringtoken=JwtUtil.parseToken(request);if(token==null){log.info("我没有从标头中获取令牌");token=request.getParameter("token");}log.info("最后令牌是:"+token);用户名密码AuthenticationTokenauthenticationToken=null;if(token!=null){StringuserName=JwtUtil.verify(token);UserDetailsuser=myUserDetailsS??ervice.loadUserByUsername(userName);authenticationToken=newUsernamePasswordAuthenticationToken(user,null,user.getAuthorities());验证ionToken.setDetails(newWebAuthenticationDetailsS??ource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);}返回空值;}@OverridepublicvoidloginFail(HttpServletRequestrequest,HRestpServlettestabove)我们在代码中已经看到了,我们只是为了测试,如果我们不能从请求头信息中获取token,那么我们可以从请求中获取parameters只是为了学习和测试偷懒,等正式项目实现的时候,这个地方还需要更加完善。启动项目,开始测试。第一步是通过登录获取token,上面已经展示了,然后使用获取到的token发送需要认证的请求。在请求参数后面加上token:如图,请求成功!上一篇SpringSecurity自定义用户认证流程(二)下一篇Mybatis拦截器