GlobalExceptionHandler处理401错误时不显示ErrorDetails详情排查
问题根因
- 认证相关逻辑运行在Servlet过滤器链层级,执行时机早于
DispatcherServlet,@ControllerAdvice仅能处理进入Spring MVC调度流程、控制器层抛出的异常,过滤器层抛出的认证类异常不会被它捕获。 - 当前全局异常处理器仅匹配
Exception.class且固定返回500状态码,未认证场景抛出的AuthenticationException、AccessDeniedException等安全框架异常会被默认异常处理器拦截,仅返回状态码,不会写入自定义的ErrorDetails响应体。 ErrorDetails类仅提供全参构造,缺少无参构造、序列化相关配置时,Jackson无法正常序列化对象,也会导致响应体为空。
解决方案
1. 自定义安全框架异常入口点,处理过滤器层认证/授权异常
过滤器层抛出的异常无法被@ControllerAdvice捕获,需要单独实现Spring Security提供的异常处理接口:
/** * 未认证异常处理器:用户未登录、Token无效、Token过期时触发 */ @Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); ErrorDetails errorDetails = new ErrorDetails( new Date(), "未认证,请先完成登录后再访问接口", request.getRequestURI() ); ObjectMapper objectMapper = new ObjectMapper(); response.getWriter().write(objectMapper.writeValueAsString(errorDetails)); response.getWriter().flush(); } } /** * 权限不足异常处理器:用户已登录但无目标接口访问权限时触发 */ @Component public class RestAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); ErrorDetails errorDetails = new ErrorDetails( new Date(), "权限不足,无法访问当前接口", request.getRequestURI() ); ObjectMapper objectMapper = new ObjectMapper(); response.getWriter().write(objectMapper.writeValueAsString(errorDetails)); response.getWriter().flush(); } }
2. 将自定义异常处理器注册到Spring Security安全链
在安全配置类中注入上述两个处理器,替换默认的异常处理逻辑:
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private RestAuthenticationEntryPoint authenticationEntryPoint; @Autowired private RestAccessDeniedHandler accessDeniedHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**", "/public/**").permitAll() .anyRequest().authenticated() ) .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); return http.build(); } }
3. 优化现有全局异常处理器
调整异常匹配逻辑,针对不同异常类型返回对应HTTP状态码,补全序列化必要的类注解:
@ControllerAdvice @ResponseBody public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { // 处理自定义业务异常 @ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorDetails> handleBusinessException(BusinessException ex, WebRequest webRequest) { ErrorDetails errorDetails = new ErrorDetails( new Date(), ex.getMessage(), webRequest.getDescription(false) ); return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST); } // 兜底处理其余未捕获异常 @ExceptionHandler(Exception.class) public ResponseEntity<ErrorDetails> handleGlobalException(Exception ex, WebRequest webRequest) { ErrorDetails errorDetails = new ErrorDetails( new Date(), "服务内部异常,请稍后重试", webRequest.getDescription(false) ); return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); } }
4. 修正ErrorDetails类配置
补全序列化需要的构造方法和注解,避免JSON序列化失败:
@Getter @Setter @NoArgsConstructor @AllArgsConstructor public class ErrorDetails { private Date timestamp; private String message; private String details; }
如果使用JWT等自定义认证过滤器,需要在过滤器内部的doFilter方法中用try-catch包裹认证逻辑,捕获到认证异常后直接转发给对应的异常入口点处理,不要直接把异常抛到外层过滤器链。
内容的提问来源于stack exchange,提问作者Andres Ramirez
相关产品推荐
相关产品推荐

