Filter exception handling and JWT renewal issues when Spring Security uses JWT verification

Problem description

When security + jwt performs identity authentication and authorization, a jwtfilter is usually added for jwt verification before logging in. Exceptions in the filter will not be captured by global exceptions, causing the return data format to be inconsistent with the unified format.

The specific description is: The exceptions thrown when verifying the distributed token in jwtfilter are captured and returned uniformly

Cause of the problem

The usual filter is provided by java web and is not taken over by spring.

Solution Throw the exception to the controller

The exception is processed in JWTFilter and jumps to the exception handling controller, and then the controller throws the exception, and is finally captured by the global exception, and the return format is unified

4 exceptions are usually thrown during jwt verification

  • ExpiredJwtException thrown when token expires
  • MalformedJwtException The format of the token is wrong, that is, the token has been tampered with
  • SignatureException token signature error, this error will be reported if the SECRET signature used to generate the token and parse the token are inconsistent
  • UnsupportedJwtException jwt is not supported

Capture these four exceptions when performing jwt verification, and then process the exception information and throw it to the controller for processing

Specific steps

1 in JwtAuthenticationTokenFilter extends OncePerRequestFilter
try {
                Claims claims = JwtUtil.parseJWT(token);
}catch (ExpiredJwtException exception){
                log.error("Authentication expired" + exception);
                request.setAttribute("jwtFilter.error",new BaseException("Authentication expired"));
                request.getRequestDispatcher("/error/jwtFilter").forward(request,response);
}catch (MalformedJwtException exception){
                log.error("JWT Token format is incorrect" + exception);
                request.setAttribute("jwtFilter.error",new BaseException("JWT Token format is incorrect"));
                request.getRequestDispatcher("/error/jwtFilter").forward(request,response);
}catch (SignatureException exception){
                //If the SECRET signature used to generate the token and parse the token are inconsistent, this error will be reported.
                log.error("JWT signature error" + exception);
                request.setAttribute("jwtFilter.error",new BaseException("JWT signature error"));
                request.getRequestDispatcher("/error/jwtFilter").forward(request,response);
}catch (UnsupportedJwtException exception){
                log.error("Unsupported Jwt " + exception);
                request.setAttribute("jwtFilter.error",new BaseException("Unsupported Jwt"));
                request.getRequestDispatcher("/error/jwtFilter").forward(request,response);
}catch (Exception e) {
                log.error("jwt verification:" + e);
                request.setAttribute("jwtFilter.error",new BaseException("JWT verification failed"));
                request.getRequestDispatcher("/error/jwtFilter").forward(request,response);
          
}
2 FilterExceptionController is eventually thrown and will be processed by the global exception handler to return a unified data format
 @RequestMapping("/error/jwtFilter")
    public void jwtFilterException(HttpServletRequest reques){
        log.warn("jwt controller ");
        Exception e = (Exception) reques.getAttribute("jwtFilter.error");
        log.warn(e.getMessage());
        throw e;
    }

Other related

Exception thrown by security during authentication and authorization

Rewrite AuthenticationEntryPoint during authentication

Override AccessDeniedHandler when authorizing

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().println(JSON.toJSON(BaseResponse.fail("1005",e.getMessage())));
        httpServletResponse.getWriter().flush();
    }
}



@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().println(JSON.toJSON(BaseResponse.fail("1004",e.getMessage())));
        httpServletResponse.getWriter().flush();
    }
}

Add it to the security configuration

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//EnableGlobalMethodSecurity turns on global method level authorization control, prePostEnabled authorizes before executing the method
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
    protected void configure(HttpSecurity http) throws Exception {
      //Prompt when authentication fails and return custom information
        http.exceptionHandling()
                .accessDeniedHandler(customAccessDeniedHandler)
                .authenticationEntryPoint(customAuthenticationEntryPoint);


    }
}

JWT renewal issue

When the jwt token is about to expire, you can choose not to renew it. After expiration, the user needs to log in again to issue the token again.

Renewal plan

1 Renewal on each request [not considered at all]

That is, a new token is issued again for each request.

Disadvantages: The delivery frequency is too high, affecting performance

2 Renewal when it is about to expire [can be considered]

For example, if the expiration time is 30 minutes, when there is less than 10 minutes left, request it to be renewed.

shortcoming:

a. After leakage, it is easy for the token to remain active.

b. And if it is a concurrent request, it is easy to cause multiple tokens to be issued.

About the leaked patch of this solution

You can add information such as IP or region to the token [this information should be encrypted]. Once you change the IP or region, you need to log in again.

But if the customer often changes IP, they will need to log in frequently and the experience will not be good. You can let the user choose the safe mode. If you choose the safe mode, you need to do this.

3 Double token [can be considered]

Two tokens are issued at one time when logging in

token1 is used for user authentication, and the expiration time is set shorter, such as 1 hour.

token2 is used for renewal. The expiration time can be set longer, such as 1 day.

The key of token2 can be set according to your own habits. You can set an irrelevant key, which can confuse it to a certain extent.

process:

The login request returns token1, token2 and the expiration time of token1

The front end determines that when token1 is about to expire, it carries token2 request.

The backend renews token1 based on token2

shortcoming:

Each front-end request needs to first determine whether token1 is about to expire.

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java Skill TreeHomepageOverview 138895 people are learning the system