Integrating spring security, spring boot and redis, jwt realizes the function of separate login authentication and picture verification code at the front and back ends

Integrating spring security, spring boot and redis, jwt realizes the function of separate login authentication and picture verification code at the front and back ends

First, we need to understand the general implementation process of the spring security filter chain:

That is, when a user initiates a request, he will enter the Security filter chain.

  • Judge whether the LogoutFilter is an exit path. If it is an exit path, go to logoutHandler to exit the processor. If the exit is successful, go to logoutSuccessHandler to exit the successful processing. If it is not the exit path, go directly to the next filter.
  • When accessing the UsernamePasswordAuthenticationFilter, judge whether it is the login path. If yes, enter the filter for login operation. If the login fails, go to the AuthenticationFailureHandler for processing. If the login succeeds, go to the AuthenticationSuccessHandler for processing, If it is not a login request, the filter is not entered.
  • Enter the authentication BasicAuthenticationFilter for user authentication. If successful, the authenticated result will be written to the attribute authentication of SecurityContext in the SecurityContextHolder. If the authentication fails, it will be handed over to the AuthenticationEntryPoint authentication failure processing class, or an exception will be thrown and handled by the subsequent ExceptionTranslationFilter filter. If it is an AuthenticationException, it will be handed over to the AuthenticationEntryPoint for processing. If it is an AccessDeniedException exception, it will be handed over to the AccessDeniedHandler for processing.
  • When you get to the FilterSecurityInterceptor, you will get the uri and find the corresponding authentication management according to the uri

LogoutFilter - logout filter
logoutSuccessHandler - operation class after successful logout
UsernamePasswordAuthenticationFilter - from submit username password login authentication filter
AuthenticationFailureHandler - login failure action class
AuthenticationSuccessHandler - login success operation class
BasicAuthenticationFilter - Basic Authentication filter
SecurityContextHolder - security context static tool class
AuthenticationEntryPoint - authentication failed entry
ExceptionTranslationFilter - exception handling filter
AccessDeniedHandler - insufficient permission operation class
FilterSecurityInterceptor - permission determination interceptor, exit

After understanding the general process, our first step is naturally to import their jar packages.

  • For the image verification code function, since we want to implement it, we first need a class to generate the image verification code, in which we set the size and size of the image:
@Configuration
public class KaptchaConfig {
   @Bean
   public DefaultKaptcha producer() {
      Properties properties = new Properties();
      properties.put("kaptcha.border", "no");
      properties.put("kaptcha.textproducer.font.color", "black");
      properties.put("kaptcha.textproducer.char.space", "4");
      properties.put("kaptcha.image.height", "40");
      properties.put("kaptcha.image.width", "120");
      properties.put("kaptcha.textproducer.font.size", "30");
      Config config = new Config(properties);
      DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
      defaultKaptcha.setConfig(config);
      return defaultKaptcha;
   }
}
  • Next, we need to write the controller that generates the verification code in the controller layer:
@Slf4j
@RestController
public class AuthController extends BaseController{
   @Autowired
   private Producer producer;
   /**
    * Picture verification code
    */
   @GetMapping("/captcha")
   public Result captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
      String code = producer.createText();
      String key = UUID.randomUUID().toString();
      BufferedImage image = producer.createImage(code);
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      ImageIO.write(image, "jpg", outputStream);
      BASE64Encoder encoder = new BASE64Encoder();
      String str = "data:image/jpeg;base64,";
      String base64Img = str + encoder.encode(outputStream.toByteArray());

      // Store in redis
      redisUtil.hset(Const.captcha_KEY, key, code, 120);
      log.info("Verification Code -- {} - {}", key, code);
      return Result.succ(
            MapUtil.builder()
            .put("token", key)
            .put("base64Img", base64Img)
            .build()
      );
   }
}
  • Since we separate the front and back ends, we need to disable the session. Here, the verification code is cached in redis. Then, each time the verification code is generated, a key is randomly generated to the front end, and the front end submits the key and the verification code together when submitting the form.
  • Then, because of the way of image verification code, we encode and base64 encode the image, so that the front end can display the image.

Due to the configuration of springsecurity, we need to set a pre filter before the original login filter to verify whether the verification code is correct:

/**
 * Picture verification code verification filter, before logging in to the filter
 */
@Slf4j
@Component
public class CaptchaFilter extends OncePerRequestFilter {
   private final String loginUrl = "/login";
   @Autowired
   RedisUtil redisUtil;
   @Autowired
   LoginFailureHandler loginFailureHandler;
   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
         throws ServletException, IOException {
      String url = request.getRequestURI();
      if (loginUrl.equals(url) && request.getMethod().equals("POST")) {
         log.info("Get login Link, verifying verification code -- " + url);
         try {
            validate(request);
         } catch (CaptchaException e) {
            log.info(e.getMessage());
            // Give it to the login failure processor for processing
            loginFailureHandler.onAuthenticationFailure(request, response, e);
         }
      }
      filterChain.doFilter(request, response);
   }
   private void validate(HttpServletRequest request) {
      String code = request.getParameter("code");
      String token = request.getParameter("token");
      if (StringUtils.isBlank(code) || StringUtils.isBlank(token)) {
         throw new CaptchaException("Verification code cannot be empty");
      }
      if(!code.equals(redisUtil.hget(Const.captcha_KEY, token))) {
         throw new CaptchaException("Incorrect verification code");
      }
      // Disposable use
      redisUtil.hdel(Const.captcha_KEY, token);
   }
}
  • After understanding the previous filter chain execution process, we know that we will have a processor to deal with login failure:
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

		response.setContentType("application/json;charset=UTF-8");
		ServletOutputStream outputStream = response.getOutputStream();

		outputStream.write(JSONUtil.toJsonStr(new ApiResponse<>().setReMsg(exception.getMessage())).getBytes("UTF-8"));

		outputStream.flush();
		outputStream.close();
	}
}
  • Similarly, for successful login, we will also have a processor:
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {


	@Autowired
	JwtUtils jwtUtils;

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {


		response.setContentType("application/json;charset=UTF-8");
		ServletOutputStream outputStream = response.getOutputStream();
		// jwt is generated and placed in the request header
		String jwt = jwtUtils.generateToken(authentication.getName());
		response.setHeader(jwtUtils.getHeader(), jwt);
		outputStream.write(JSONUtil.toJsonStr(new ApiResponse<>().setReMsg("Login succeeded")).getBytes("UTF-8"));
		outputStream.flush();
		outputStream.close();
	}

}
  • Then, in order to combine jwt, we need a jwt tool class:
@Data
@Component
@ConfigurationProperties(prefix = "matrix.jwt")
public class JwtUtils {

	private long expire;
	private String secret;
	private String header;

	// Generate jwt
	public String generateToken(String username) {

		Date nowDate = new Date();
		Date expireDate = new Date(nowDate.getTime() + 1000 * expire);

		return Jwts.builder()
				.setHeaderParam("typ", "JWT")
				.setSubject(username)
				.setIssuedAt(nowDate)
				.setExpiration(expireDate)// 7 days overdue
				.signWith(SignatureAlgorithm.HS512, secret)
				.compact();
	}

	// Parse jwt
	public Claims getClaimByToken(String jwt) {
		try {
			return Jwts.parser()
					.setSigningKey(secret)
					.parseClaimsJws(jwt)
					.getBody();
		} catch (Exception e) {
			return null;
		}
	}

	// jwt expired
	public boolean isTokenExpired(Claims claims) {
		return claims.getExpiration().before(new Date());
	}

}
  • This tool class implements the generation and verification of token s, and the key information should be written in the yml configuration file:
matrix:
  jwt:
    header: authorization
    expire: 604800
    secret: ji8n3439n439n43ld9ne9343fdfer49h
  • Because we have implemented jwt technology, we also need a jwt filter to handle the case with token:
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

	@Autowired
	UserService userService;

	@Autowired
	JwtUtils jwtUtils;
	@Autowired
	UserDetailServiceImpl userDetailService;
	public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
		super(authenticationManager);
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

		String jwt = request.getHeader(jwtUtils.getHeader());
		if (StrUtil.isBlankOrUndefined(jwt)) {
			// No jwt direct release
			chain.doFilter(request, response);
			return;
		}
		System.out.println(12312);

		Claims claim = jwtUtils.getClaimByToken(jwt);
		if (claim == null) {
			throw new JwtException("token abnormal");
		}
		if (jwtUtils.isTokenExpired(claim)) {
			// Will be injected into the exception that failed validation
			throw new JwtException("token Expired ");
		}

		String username = claim.getSubject();
		// Obtain user permissions and other information
		User user = userService.getByUsername(username);

		// Store token information
		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(user.getId()));


		SecurityContextHolder.getContext().setAuthentication(token);
		// Release
		chain.doFilter(request, response);
	}
}
  • We also need a processor to solve the exceptions when anonymous users access unauthorized resources:
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

		response.setContentType("application/json;charset=UTF-8");
		response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
		ServletOutputStream outputStream = response.getOutputStream();

		outputStream.write(JSONUtil.toJsonStr(new ApiResponse<>().setReMsg("Please log in first")).getBytes("UTF-8"));

		outputStream.flush();
		outputStream.close();
	}
}
  • Similarly, our passwords cannot be stored in plaintext. We need to configure encryption, so we need to inject encryption and authentication policies:
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
   return new BCryptPasswordEncoder();
}

Keywords: Java Redis Spring Boot Spring Security jwt

Added by Kevin3374 on Mon, 17 Jan 2022 15:33:09 +0200