共计 7378 个字符,预计需要花费 19 分钟才能阅读完成。
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,咱们须要减少一项配置,所以咱们要新建一个配置文件:
@Configuration
public 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
@Component
public 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)