自定义过滤器: Spring Security 前后端分离 + JWT + 验证码 + Redis 项目开发笔记

更改Security认证流程

实现自定义认证的核心: 自定义过滤器+更改过滤器链顺序.

Security过滤器链

Security过滤器链.png

更改Security认证流程, 实际上就是更改Security过滤器链.

UsernamePasswordAuthenticationFilter是默认的认证过滤器, Security默认的表单登录就是通过此过滤器实现. 所以为了自定义Jwt认证, 就要写一个Jwt过滤器, 并添加在UsernamePasswordAuthenticationFilter前面, 完成认证的动作.

自定义JwtAuthenticationTokenFilter

作用:

  1. 有合法且未过期token: 载入用户信息, 并放行给下游过滤器进一步处理.
  2. 非法或已过期token: 终止过滤器链并返回错误信息.

自定义过滤器流程:

  1. 继承OncePerRequestFilter: 避免同一个请求重复进入过滤器.
  2. 重写doFilterInternal方法.

图解JwtAuthenticationTokenFilter:

JwtAuthenticationTokenFilter.drawio.png

问题:

  1. doFilterInternal方法返回类型为void, 而前后端分离需要后端返回json类型的数据.
  2. 用户信息从何处获取.

解决方案:

  1. 在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
    复制代码
  2. 需要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);
	}
}
复制代码

踩坑:

  1. writer应现用现取:

尝试考虑在方法中先获取writer: PrintWriter output = response.getWriter(); 发现最后放行代码使用response时报错.

  1. 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);
	}
复制代码

猜你喜欢

转载自juejin.im/post/7080682078815125517