spring boot關於在進入controller層之前捕捉異常


問題

spring boot中使用全局異常捕捉器捕捉異常返回友好數據, 准確地說不應該叫做全局異常捕捉器, 因為@RestControllerAdvice定義的異常捕捉只能捕捉經過controller層的異常, 而進入controller層之前的異常, 比如進入controller層之前的過濾器中的異常, 無法被捕捉

那么如何捕捉進入controller層之前的異常?

場景

spring security + jwt安全權限框架

用戶發起一個請求, 首先經過過濾器檢驗是否帶token或token是否合法, 不合法就不從數據中加載用戶數據, 合法就加載用戶數據和權限到上下文中

// AuthenticationFilter
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
  String token = jwtUtil.getTokenFromRequest(request); // 從request中解析token
  // 沒token的直接玩完
  if (StrUtil.isNotEmpty(token)) {
      // token過期的 或偽造 或多設備登錄的 直接玩完
      String username = jwtUtil.getUsernameFromToken(token); // 從token中解析username
      if (StrUtil.isNotEmpty(username) && ObjectUtil.isNull(SecurityUtil.getCurrentAuthentication())) {
          // 合法, 用戶加載信息
          UserDetails userDetails = userDetailsService.loadUserByUsername(username);
          UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, null);
          SecurityUtil.getContext().setAuthentication(authenticationToken);
      }
  }
  filterChain.doFilter(request, response);
}

// jwtUtil
/**
 * 將token解析為Claims
 * @param token token
 * @throws TokenExpiredException         token過期
 * @throws IllegalTokenException         不合法的token
 * @throws OtherClientsLoggedInException 已在其它客戶端登錄
 */
public Claims parseToken(String token) {
    Claims claims;
    String username;
    try {
        claims = Jwts.parser()
                .setSigningKey(this.secret)
                .parseClaimsJws(token) // 解析token 拋出ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException捕捉后再拋出自定義的異常
                .getBody();
        username = claims.getSubject();
      
    } catch (ExpiredJwtException e) {
        log.error("JwtUtil - Token 已過期");
        throw new TokenExpiredException("token已過期");
    } catch (UnsupportedJwtException e) {
        log.error("JwtUtil - 不支持的token");
        throw new IllegalTokenException("不支持的token");
    } catch (MalformedJwtException e) {
        log.error("JwtUtil - token無效");
        throw new IllegalTokenException("token無效");
    } catch (SignatureException e) {
        log.error("JwtUtil - 無效的token簽名");
        throw new IllegalTokenException("無效的token簽名");
    } catch (IllegalArgumentException e) {
        log.error("JwtUtil - token參數不存在");
        throw new IllegalTokenException("JwtUtil - token參數不存在");
    }

    String redisKey = this.redisTokenPrefix + username;
    if (redisService.isExpired(redisKey)) {
        log.error("JwtUtil - redis中的token已過期");
        throw new TokenExpiredException("token已過期");
    }
    // 檢驗redis中的token是否與當前一致, 不一致則代表用戶已注銷/用戶在不同設備登錄,均代表JWT已過期
    String redisToken = redisService.get(redisKey);
    if (!StrUtil.equals(token, redisToken)) {
        log.error("JwtUtil - redis中的token不一致");
        throw new OtherClientsLoggedInException("已在其它客戶端登錄, 請重新登錄");
    }
    return claims;
}

/**
 * 從token中解析username
 */
public String getUsernameFromToken(String token) {
    Claims claims = parseToken(token);
    return claims.getSubject();
}
// GlobalExceptionHandler 全局異常處理
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalTokenException.class)
    public Dict IllegalTokenException(HttpServletRequest request, HttpServletResponse response, IllegalTokenException e) {
        log.error("GlobalExceptionHandler - IllegalTokenException - {}", e.getMessage());
        return Result.illegalToken(e.getMessage());
    }

    @ExceptionHandler(TokenExpiredException.class)
    public Dict tokenExpiredException(HttpServletRequest request, HttpServletResponse response,TokenExpiredException e) {
        log.error("GlobalExceptionHandler - TokenExpiredException - {}", e.getMessage());
        return Result.tokenExpired(e.getMessage());
    }

    @ExceptionHandler(OtherClientsLoggedInException.class)
    public Dict otherClientsLoggedInException(HttpServletRequest request, HttpServletResponse response,OtherClientsLoggedInException e) {
        log.error("GlobalExceptionHandler - OtherClientsLoggedInException - {}", e.getMessage());
        return Result.otherClientsLoggedIn(e.getMessage());
}

全局異常處理返回友好數據, 其中Result是封裝HuTool的Dict字典類作為統一返回對象

使用不合法的token發起一次請求


圖1 構造無效的token發起請求

根據圖1可以看到, 拋出了異常, 但GlobalExceptionHandler未能正確捕捉異常返回友好數據

解決

可行的做法一[1]

(推薦)在過濾器中捕捉異常並直接利用response返回結果

// AuthenticationFilter
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
    String token = jwtUtil.getTokenFromRequest(request);
    // 沒token的直接玩完
    if (StrUtil.isNotEmpty(token)) {
        // token過期的 或偽造 或多設備登錄的 直接玩完
        String username;
        try {
            // filter中的異常, GlobalExceptionHandler無法捕捉, 轉發到ExceptionController中進行統一返回
            username = jwtUtil.getUsernameFromToken(token);
        } catch (Exception e) {
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "*");
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(200);
            // 將Dict對象轉化為JSON字符串寫入response
            if (e instanceof TokenExpiredException) {
                // 調用統一封裝對象的tokenExpired()
                response.getWriter()
                        .write(JSONUtil.toJsonStr(Result.tokenExpired(e.getMessage())));
            } else if (e instanceof IllegalTokenException) {
                // 調用統一封裝對象的illegalToken()
                response.getWriter()
                        .write(JSONUtil.toJsonStr(Result.illegalToken(e.getMessage())));
            } else if (e instanceof OtherClientsLoggedInException) {
                // 調用統一封裝對象的otherClientsLoggedIn()
                response.getWriter()
                        .write(JSONUtil.toJsonStr(Result.otherClientsLoggedIn(e.getMessage())));
            }else{
                response.getWriter()
                        .write(JSONUtil.toJsonStr(Result.forbidden(e.getMessage())));
            }
            return;
        }
        if (StrUtil.isNotEmpty(username) && ObjectUtil.isNull(SecurityUtil.getCurrentAuthentication())) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, null);
            SecurityUtil.getContext().setAuthentication(authenticationToken);
        }
    }
    filterChain.doFilter(request, response);   
}

圖2 構造無效token發起請求

可以看到, 這種方法行得通, 返回了自定義的封裝結果

可行的做法二[2]

在過濾器中捕捉異常, 利用request的轉發, 轉發到特定的controller進行異常的返回

創建專門返回異常的controller

// ExceptionController
@RestController
@RequestMapping("/exception")
public class ExceptionController {
    @RequestMapping("/token-expired-exception")
    public Dict tokenExpiredException(HttpServletRequest request) {
        String msg = (String) request.getAttribute("msg");
        return Result.tokenExpired(msg);
    }

    @RequestMapping("/illegal-token-exception")
    public Dict illegalTokenException(HttpServletRequest request) {
        String msg = (String) request.getAttribute("msg");
        return Result.illegalToken(msg);
    }

    @RequestMapping("/other-clients-logged-in-exception")
    public Dict otherClientsLoggedInException(HttpServletRequest request) {
        String msg = (String) request.getAttribute("msg");
        return Result.otherClientsLoggedIn(msg);
    }

    @RequestMapping("/access-denied-exception")
    public Dict accessDeniedException(HttpServletRequest request) {
        String msg = (String) request.getAttribute("msg");
        return Result.forbidden(msg);
    }

    @RequestMapping("/authentication-entry-point-exception")
    public Dict authenticationEntryPoint(HttpServletRequest request) {
        String msg = (String) request.getAttribute("msg");
        return Result.forbidden(msg);
    }

    @RequestMapping("/error")
    public Dict error(HttpServletRequest request) {
        String msg = (String) request.getAttribute("msg");
        return Result.otherClientsLoggedIn(msg);
    }
}

// Authentication
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
    String token = jwtUtil.getTokenFromRequest(request);
    // 沒token的直接玩完
    if (StrUtil.isNotEmpty(token)) {
        // token過期的 或偽造 或多設備登錄的 直接玩完
        String username = null;
        try {
            // filter中的異常, GlobalExceptionHandler無法捕捉, 轉發到ExceptionController中進行統一返回
            username = jwtUtil.getUsernameFromToken(token);
        } catch (Exception e) {
            request.setAttribute("msg", e.getMessage()); // 設置異常信息
            String url;
            if (e instanceof TokenExpiredException) {
                url = "/exception/token-expired-exception";
            } else if (e instanceof IllegalTokenException) {
                url = "/exception/illegal-token-exception";
            } else if (e instanceof OtherClientsLoggedInException) {
                url = "/exception/other-clients-logged-in-exception";
            } else {
                url = "/exception/error";
            }
            request.getRequestDispatcher(url).forward(request, response);
            return;
        }
        if (StrUtil.isNotEmpty(username) && ObjectUtil.isNull(SecurityUtil.getCurrentAuthentication())) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, null);
            SecurityUtil.getContext().setAuthentication(authenticationToken);
        }
    }
    filterChain.doFilter(request, response);
}


圖3 構造無效token發起請求

這種方法雖然可以catch到異常后轉發到專門的controller中方便管理, 但有時候會直接跳過request.getRequestDispatcher(url).forward(request, response);
並不會進行轉發, 但有時也能正常轉發, 很讓人頭疼

方法一可以在catch到異常后准確地將異常信息發送到瀏覽器

參考

[1]. SpringBoot統一異常攔截處理(filter中的異常無法被攔截處理)
[2]. SpringBoot全局異常處理捕獲Filter內部異常


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM