@PreAuthorize抛出的AccessDeniedException无法被@ExceptionHandler捕获
场景描述
配置了全局异常处理器GlobalExceptionHandler,其中包含AccessDeniedException的处理方法,但使用@PreAuthorize注解的方法授权失败时,该异常无法被捕获。从日志可见,Spring Security默认的AccessDeniedHandlerImpl提前处理了请求,直接返回403状态码,导致请求转发到/error端点,全局异常处理器无法介入。
相关代码
全局异常处理器
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BadCredentialsException.class) public ResponseEntity<ApiError> handleBadCredentials(Exception e, HttpServletRequest request) { ApiError apiError = new ApiError( request.getRequestURI(), e.getMessage(), HttpStatus.BAD_REQUEST.value(), LocalDateTime.now() ); return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity<ApiError> handleMethodNotSupportedException(Exception e, HttpServletRequest request) { ApiError apiError = new ApiError( request.getRequestURI(), e.getMessage(), HttpStatus.BAD_REQUEST.value(), LocalDateTime.now() ); return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<ApiError> handleAccessDeniedException(Exception e, HttpServletRequest request) { ApiError apiError = new ApiError( request.getRequestURI(), e.getMessage(), HttpStatus.FORBIDDEN.value(), LocalDateTime.now() ); return new ResponseEntity<>(apiError, HttpStatus.FORBIDDEN); } @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity<ApiError> handleEntityNotFound(Exception e, HttpServletRequest request) { ApiError apiError = new ApiError( request.getRequestURI(), e.getMessage(), HttpStatus.NOT_FOUND.value(), LocalDateTime.now() ); return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<ApiError> handleIllegalArgument(Exception e, HttpServletRequest request) { ApiError apiError = new ApiError( request.getRequestURI(), e.getMessage(), HttpStatus.BAD_REQUEST.value(), LocalDateTime.now() ); return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST); } }
ApiError 类
public record ApiError( String path, String message, int statusCode, LocalDateTime date ) { }
Security 配置
@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Autowired private UserDetailsCustomImpl userDetailsService; @Autowired private AuthenticationProvider authenticationProvider; @Autowired private JwtFilter jwtFilter; @Bean public SecurityFilterChain securityfilterChain(HttpSecurity http) throws Exception { http.csrf((csrf) -> { csrf.ignoringRequestMatchers( AntPathRequestMatcher.antMatcher("/h2-console/**")); csrf.disable(); } ); http.headers((headers) -> { headers.frameOptions((frameOption) -> frameOption.disable()); }); http .authorizeHttpRequests(auth -> auth .requestMatchers(AntPathRequestMatcher.antMatcher("/h2-console/**"), AntPathRequestMatcher.antMatcher("/api/v1/auth/**")).permitAll() .anyRequest().authenticated()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authenticationProvider(authenticationProvider) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } public void configureAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } public void configureIgnores(WebSecurity web) { web .ignoring() .requestMatchers("/h2-console/**"); } }
关键日志片段
2023-08-03T15:05:56.930+02:00 DEBUG 22212 --- [nio-8080-exec-7] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code 2023-08-03T15:05:56.932+02:00 DEBUG 22212 --- [nio-8080-exec-7] o.s.security.web.FilterChainProxy : Securing PATCH /error
解决方案
核心原因
Spring Security的过滤器链优先级高于Spring MVC的DispatcherServlet,当方法级授权失败抛出AccessDeniedException时,默认的AccessDeniedHandlerImpl会直接向客户端返回403响应,不会将异常传递给DispatcherServlet,因此全局异常处理器无法捕获该异常。
方案一:自定义AccessDeniedHandler,抛出异常让全局处理器捕获
创建自定义的AccessDeniedHandler实现,在处理方法中重新抛出AccessDeniedException,使异常能够进入Spring MVC的异常处理流程:
@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { // 重新抛出异常,交给Spring MVC的全局异常处理器处理 throw accessDeniedException; } }
然后在Security配置中替换默认的handler:
@Autowired private CustomAccessDeniedHandler customAccessDeniedHandler; @Bean public SecurityFilterChain securityfilterChain(HttpSecurity http) throws Exception { // ... 原有配置 // 配置自定义的AccessDeniedHandler http.exceptionHandling(exceptionConfig -> exceptionConfig.accessDeniedHandler(customAccessDeniedHandler) ); return http.build(); }
方案二:直接在AccessDeniedHandler中返回自定义ApiError响应
如果不想依赖全局异常处理器,也可以在自定义handler中直接生成并返回ApiError格式的JSON响应:
@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { private final ObjectMapper objectMapper; // 注入Spring默认的ObjectMapper public CustomAccessDeniedHandler(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setStatus(HttpStatus.FORBIDDEN.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); ApiError apiError = new ApiError( request.getRequestURI(), accessDeniedException.getMessage(), HttpStatus.FORBIDDEN.value(), LocalDateTime.now() ); // 将ApiError序列化为JSON写入响应 objectMapper.writeValue(response.getWriter(), apiError); } }
同样在Security配置中配置该handler即可。
内容的提问来源于stack exchange,提问作者Thorvas
相关产品推荐
相关产品推荐

