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

Spring Security全局异常处理器中区分401与403 AccessDeniedException

如何区分Spring Security中的401(未授权)和403(禁止)异常?

问题场景

在Spring Boot应用中集成Spring Security后,默认逻辑可以正确返回对应状态码:

  • 未登录访问受保护接口时返回401(Unauthorized)
  • 登录后权限不足时返回403(Forbidden)

对应的单元测试可验证该行为:

@Test
void notLoggedIn() throws Exception {
    mockMvc.perform(get("/service")) //
            .andExpect(status().isUnauthorized()); // 捕获401
}

@Test
void loggedInWithIncorrectRole() throws Exception {
    mockMvc.perform(get("/service").with(user("someuser").roles("invalidRole"))) //
            .andExpect(status().isForbidden());    // 捕获403
}

但添加@ControllerAdvice全局异常处理器捕获所有异常后,所有安全异常都以org.springframework.security.access.AccessDeniedException: Access Denied的形式进入处理器,导致401和403无法区分:

@ControllerAdvice
public class AppExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e, WebRequest request) {
        log.error(e);
        if ("org.springframework.security.access.AccessDeniedException".equals(e.getClass().getName())) {
            // 同时捕获401和403,无法区分
            return new ResponseEntity<ErrorResponse>(HttpStatus.UNAUTHORIZED);
        } else {
            return ResponseEntity.internalServerError()
                    .body(new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Error occurred"));
        }
    }
}

对应的异常堆栈始终为:

org.springframework.security.access.AccessDeniedException: Access Denied
    at org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.attemptAuthorization(AuthorizationManagerBeforeMethodInterceptor.java:256)
    at org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.invoke(AuthorizationManagerBeforeMethodInterceptor.java:197)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:717)

解决方案

方案一:使用Spring Security原生异常处理(推荐)

Spring Security本身提供了专门处理未认证和权限不足的扩展点,不需要通过@ControllerAdvice介入,配置后可直接区分两种状态码:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        // 其他安全配置(如csrf、授权规则等)...
        .exceptionHandling(exceptions -> exceptions
            // 处理未认证场景(返回401)
            .authenticationEntryPoint((request, response, authException) -> {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                ErrorResponse errorResponse = new ErrorResponse(HttpStatus.UNAUTHORIZED, "未登录或认证凭证已失效");
                new ObjectMapper().writeValue(response.getWriter(), errorResponse);
            })
            // 处理权限不足场景(返回403)
            .accessDeniedHandler((request, response, accessDeniedException) -> {
                response.setStatus(HttpStatus.FORBIDDEN.value());
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                ErrorResponse errorResponse = new ErrorResponse(HttpStatus.FORBIDDEN, "权限不足,无法访问该资源");
                new ObjectMapper().writeValue(response.getWriter(), errorResponse);
            })
        );
    return http.build();
}

这种方式遵循Spring Security的设计逻辑,避免全局异常处理器干扰安全流程,是最稳妥的实现方式。

方案二:在@ControllerAdvice中通过认证状态区分

如果必须使用全局异常处理器,可通过SecurityContextHolder获取当前用户的认证状态,判断是未登录还是权限不足:

@ControllerAdvice
public class AppExceptionHandler {
    
    private static final Logger log = LoggerFactory.getLogger(AppExceptionHandler.class);
    
    // 单独处理AccessDeniedException
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException e) {
        log.error("权限校验失败", e);
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        // 匿名用户表示未登录,返回401
        if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new ErrorResponse(HttpStatus.UNAUTHORIZED, "未登录或认证凭证已失效"));
        }
        // 已登录但权限不足,返回403
        else {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                    .body(new ErrorResponse(HttpStatus.FORBIDDEN, "权限不足,无法访问该资源"));
        }
    }
    
    // 处理其他全局异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
        log.error("系统异常", e);
        return ResponseEntity.internalServerError()
                .body(new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "系统发生未知异常"));
    }
}

这里的核心逻辑是:未登录时,SecurityContext中的认证对象为AnonymousAuthenticationToken(或null),已登录但权限不足时则是已认证的用户对象,通过这一点可以区分两种场景。

内容的提问来源于stack exchange,提问作者gene b.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 00:13:14