Custom filter: Spring Security front-end and back-end separation + JWT + verification code + Redis project development notes

Change the Security Authentication Process

The core of implementing custom authentication: custom filter + change filter chain order.

Security filter chain

Security filter chain.png

Changing the Security authentication process is actually changing the Security filter chain.

UsernamePasswordAuthenticationFilter is the default authentication filter, and Security's default form login is implemented through this filter. So in order to customize Jwt authentication, it is necessary to write a Jwt filter and add it in front of UsernamePasswordAuthenticationFilter to complete the authentication action.

Custom JwtAuthenticationTokenFilter

effect:

  1. There is a valid and unexpired token: Load user information and pass it to downstream filters for further processing.
  2. Illegal or expired token: terminates the filter chain and returns an error message.

Custom filter process:

  1. Inherit OncePerRequestFilter: Avoid repeated entry of the same request into the filter.
  2. Override the doFilterInternal method.

Graphical JwtAuthenticationTokenFilter:

JwtAuthenticationTokenFilter.drawio.png

question:

  1. The return type of the doFilterInternal method is void, and the front-end and back-end separation requires the back-end to return json-type data.
  2. Where to get user information from.

solution:

  1. Return json type data in HttpServletResponse: // Define all methods in the class Return type: 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. There are many interface requests that require tokens, so user information should be stored in redis after logging in. After that, token authentication will obtain user information from redis.

Code:

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

Step on the pit:

  1. The writer should use and withdraw:

Try to consider getting the writer first in the method: PrintWriter output = response.getWriter(); It is found that the last release code uses the response to report an error.

  1. The exception thrown when the token expires can never be caught and handled:

Instead, catch the exception in the JwtUtils class instead of in the filter (guess that it is related to ExceptionTranslationFilter). Then judge whether it expires in the filter.

Refer to Analysis of the solution to the ExpiredJwtException when the token expires in JWT and how to carry out subsequent business processing after the expiration . The solution given, you can take out the claims before throwing the exception, and transform the code for parsing the token into:

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
	}
复制代码

Instead of relying on the ExpiredJwtException thrown out to judge whether it has expired, it is necessary to add a method to judge whether the token has expired in JwtUtils, which is convenient for the filter to call and judge:

public static Boolean isTokenExpired(String token) {
		// 解析claims对象信息
		Claims claims = JwtUtils.parseJWT(token);
		Date expiration = claims.getExpiration();
		return new Date(System.currentTimeMillis()).after(expiration);
	}
复制代码

Guess you like

Origin juejin.im/post/7080682078815125517