You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

@PreAuthorize抛出的AccessDeniedException无法被@ExceptionHandler捕获

问题:自定义@ExceptionHandler无法捕获@PreAuthorize抛出的AccessDeniedException

场景描述

配置了全局异常处理器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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 18:07:03