前后端分离

1.0 Session

有状态:用户请求接口 -> 从Session中读取用户信息 -> 根据当前的用户来处理业务 -> 返回

缺点:不支持分布式

2.0 Token

无状态:用户携带Token请求接口 -> 从请求中获取用户信息 -> 根据当前的用户来处理业务 -> 返回

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.3.0</version>
</dependency>
  • 工具类
public class JwtUtils {
//Jwt秘钥
private static final String key = "abcdefghijklmn";

// 根据用户信息创建Jwt令牌
public static String createJwt(UserDetails user){
Algorithm algorithm = Algorithm.HMAC256(key);
Calendar calendar = Calendar.getInstance();
Date now = calendar.getTime();
calendar.add(Calendar.SECOND, 3600 * 24 * 7);
return JWT.create()
.withClaim("name", user.getUsername()) // 配置JWT自定义信息
.withClaim("authorities", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList())
.withExpiresAt(calendar.getTime()) // 设置过期时间
.withIssuedAt(now) // 设置创建创建时间
.sign(algorithm); // 最终签名
}

// 根据Jwt验证并解析用户信息
public static UserDetails resolveJwt(String token){
Algorithm algorithm = Algorithm.HMAC256(key);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
try {
DecodedJWT verify = jwtVerifier.verify(token); // 对JWT令牌进行验证,看看是否被修改
Map<String, Claim> claims = verify.getClaims(); // 获取令牌中内容
if(new Date().after(claims.get("exp").asDate())) // 如果是过期令牌则返回null
return null;
else
// 重新组装为UserDetails对象,包括用户名、授权信息等
return User
.withUsername(claims.get("name").asString())
.password("")
.authorities(claims.get("authorities").asArray(String.class))
.build();
} catch (JWTVerificationException e) {
return null;
}
}
}
  • Filter
public class JwtAuthenticationFilter extends OncePerRequestFilter {  
// 继承OncePerRequestFilter表示每次请求过滤一次,用于快速编写JWT校验规则

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 首先从Header中取出JWT
String authorization = request.getHeader("Authorization");
// 判断是否包含JWT且格式正确
if (authorization != null && authorization.startsWith("Bearer ")) {
String token = authorization.substring(7);
// 开始解析成UserDetails对象,如果得到的是null说明解析失败,JWT有问题
UserDetails user = JwtUtils.resolveJwt(token);
if(user != null) {
// 验证没有问题,那么就可以开始创建Authentication了,这里我们跟默认情况保持一致
// 使用UsernamePasswordAuthenticationToken作为实体,填写相关用户信息进去
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 然后直接把配置好的Authentication塞给SecurityContext表示已经完成验证
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// 最后放行,继续下一个过滤器
// 可能各位小伙伴会好奇,要是没验证成功不是应该拦截吗?这个其实没有关系的
// 因为如果没有验证失败上面是不会给SecurityContext设置Authentication的,后面直接就被拦截掉了
// 而且有可能用户发起的是用户名密码登录请求,这种情况也要放行的,不然怎么登录,所以说直接放行就好
filterChain.doFilter(request, response);
}
}
  • Security修改为无状态
// 将Session管理创建策略改成无状态,这样SpringSecurity就不会创建会话了,也不会采用之前那套机制记录用户,因为现在我们可以直接从JWT中获取信息
.sessionManagement(conf -> {
conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
})
// 添加我们用于处理JWT的过滤器到Security过滤器链中,注意要放在UsernamePasswordAuthenticationFilter之前
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)

JWT退出登录

采用黑名单方案。一台服务器存储JWT黑名单,共享给所有微服务。

JWT.create()
// 额外添加一个UUID用于记录黑名单,将其作为JWT的ID属性jti
.withJWTId(UUID.randomUUID().toString())
public class JwtUtils {    

private static final HashSet<String> blackList = new HashSet<>();
// 加入黑名单方法
public static boolean invalidate(String token){
Algorithm algorithm = Algorithm.HMAC256(key);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
try {
DecodedJWT verify = jwtVerifier.verify(token);
Map<String, Claim> claims = verify.getClaims();
//取出UUID丢进黑名单中
return blackList.add(verify.getId());
} catch (JWTVerificationException e) {
return false;
}
}

public static UserDetails resolveJwt(String token){
Algorithm algorithm = Algorithm.HMAC256(key);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
try {
DecodedJWT verify = jwtVerifier.verify(token);
// 判断是否存在于黑名单中,如果存在,则返回null表示失效
if(blackList.contains(verify.getId()))
return null;
Map<String, Claim> claims = verify.getClaims();
if(new Date().after(claims.get("exp").asDate()))
return null;
return User
.withUsername(claims.get("name").asString())
.password("")
.authorities(claims.get("authorities").asArray(String.class))
.build();
} catch (JWTVerificationException e) {
return null;
}
}
}