• 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.setContentType("application/json;charset=UTF-8");
                response.setStatus(200);
                // 将Dict对象转化为JSON字符串写入response
                if (e instanceof TokenExpiredException) {
                    // 调用统一封装对象的tokenExpired()
                    response.getWriter().write(JSONUtil.parse(new JSONObject(Result.tokenExpired(e.getMessage()))).toStringPretty());
                } else if (e instanceof IllegalTokenException) {
                    // 调用统一封装对象的illegalToken()
                    response.getWriter().write(JSONUtil.parse(new JSONObject(Result.illegalToken(e.getMessage()))).toStringPretty());
                } else if (e instanceof OtherClientsLoggedInException) {
                    // 调用统一封装对象的otherClientsLoggedIn()
                    response.getWriter().write(JSONUtil.parse(new JSONObject(Result.otherClientsLoggedIn(e.getMessage()))).toStringPretty());
                }
                return; // 直接退出, 而不需经过过滤器后部分(清除request数据等操作
            }
            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发起请求

    也能正确的处理异常

    个人倾向方法二, 转发到专门返回Exception的controller中, 方便管理

    转发方法也可以在controller中光抛出异常, 然后再在GlobalExceptionHandler中统一处理返回异常

    参考

    [1]. SpringBoot统一异常拦截处理(filter中的异常无法被拦截处理)
    [2]. SpringBoot全局异常处理捕获Filter内部异常

  • 相关阅读:
    图的深度遍历
    判断森林中有多少棵树
    基于邻接矩阵的广度优先搜索
    第三届程序设计知识竞赛网络赛
    大数相乘
    a+b=x,ab=y
    poj3278
    不敢死队
    单链表中重复元素删除
    poj2506
  • 原文地址:https://www.cnblogs.com/xfk1999/p/spring-boot-catch-exception-before-controller-presentation.html
Copyright © 2020-2023  润新知