更改Security认证流程
实现自定义认证的核心: 自定义过滤器+更改过滤器链顺序.
Security过滤器链
更改Security认证流程, 实际上就是更改Security过滤器链.
UsernamePasswordAuthenticationFilter是默认的认证过滤器, Security默认的表单登录就是通过此过滤器实现. 所以为了自定义Jwt认证, 就要写一个Jwt过滤器, 并添加在UsernamePasswordAuthenticationFilter前面, 完成认证的动作.
自定义JwtAuthenticationTokenFilter
作用:
- 有合法且未过期token: 载入用户信息, 并放行给下游过滤器进一步处理.
- 非法或已过期token: 终止过滤器链并返回错误信息.
自定义过滤器流程:
- 继承OncePerRequestFilter: 避免同一个请求重复进入过滤器.
- 重写doFilterInternal方法.
图解JwtAuthenticationTokenFilter:
问题:
- doFilterInternal方法返回类型为void, 而前后端分离需要后端返回json类型的数据.
- 用户信息从何处获取.
解决方案:
-
在HttpServletResponse中返回json类型数据: // 类中定义所有方法返回类型: private static final String CONTENT_TYPE = "application/json;charset=UTF-8";
// 方法中设置Json返回消息体: response.setCharacterEncoding("UTF-8"); response.setContentType(CONTENT_TYPE); JSONObject res = new JSONObject(); // 相当于一个map 复制代码
-
需要token的接口请求较多, 所以应在登录后将用户信息存入redis. 此后的token认证都从redis中获取用户信息.
代码:
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final String CONTENT_TYPE = "application/json;charset=UTF-8";
@Autowired
private RedisUtils redisUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ExpiredJwtException, MalformedJwtException {
// 定义Json返回消息体
response.setCharacterEncoding("UTF-8");
response.setContentType(CONTENT_TYPE);
JSONObject res = new JSONObject(); // 相当于一个map
// 获取token: 认证后的前端请求应该在header中刷入token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行:将请求交给后面的过滤器处理
filterChain.doFilter(request, response);
// 卫语句:终止此过滤器
return;
}
// 校验Token
try {
if(JwtUtils.isTokenExpired(token)) {
res.put("code", 404);
res.put("msg", "token已过期");
PrintWriter output = response.getWriter(); // 先获取getWriter后使用response会报错,所以用到writer应现用现取, 减少对后面代码使用response的影响
output.append(res.toString());
return; // 过期终止执行
}
} catch (RuntimeException e) {
res.put("code", 400);
res.put("msg", "token非法");
PrintWriter output = response.getWriter();
output.append(res.toString());
return; // 过期终止执行
}
// 合法未过期的token解析
Claims claims = JwtUtils.parseJWT(token);
String id = claims.getSubject();
// 从redis中获取用户信息
User user = (User) redisUtils.get("login:" + id);
if(Objects.isNull(user)) {
res.put("code", 410);
res.put("msg", "token已注销");
PrintWriter output = response.getWriter();
output.append(res.toString());
return; // 终止执行
}
// 存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); // 三参数的此方法传入已登录用户,方法内部将其标记为已登录
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
复制代码
踩坑:
- writer应现用现取:
尝试考虑在方法中先获取writer: PrintWriter output = response.getWriter(); 发现最后放行代码使用response时报错.
- token过期时抛出的异常始终无法被捕获处理:
改由在JwtUtils类中捕获异常, 而不是在filter中捕获(猜测无法捕获与ExceptionTranslationFilter有关). 然后在filter中判断是否过期.
参考 浅析JWT中token过期后解析报错ExpiredJwtException的解决及过期之后如何进行后续业务处理 给出的解决方案, 可以在抛出异常前将claims拿出来, 解析token的代码改造为:
public static Claims parseJWT(String token) {
// 使用DefaultJwtParser解析
Claims claims;
try {
claims = Jwts.parser()
// 设置签名密钥
.setSigningKey(SIGN.getBytes(StandardCharsets.UTF_8))
// 设置被解析jwt
.parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
claims = e.getClaims(); // 无论是否过期,都能拿到claims
} catch (SignatureException e) {
throw new RuntimeException("签名异常");
}
return claims; // 始终返回claims
}
复制代码
不依靠throw出来的ExpiredJwtException判断是否过期, 就要在JwtUtils中添加一个判断是否token是否过期的方法, 便于过滤器调用判断:
public static Boolean isTokenExpired(String token) {
// 解析claims对象信息
Claims claims = JwtUtils.parseJWT(token);
Date expiration = claims.getExpiration();
return new Date(System.currentTimeMillis()).after(expiration);
}
复制代码