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.
相关产品推荐
相关产品推荐

