如何在Spring Security中自定义401与403错误的异常处理
Spring Security + JWT 自定义401/403响应体方案
首先明确Spring Security中对应异常类型:
- 401(Token缺失/无效):对应
AuthenticationException及其子类(如BadCredentialsException、InsufficientAuthenticationException,具体取决于你的JWT验证逻辑) - 403(权限不足):对应
AccessDeniedException
注意:这些异常是在Spring Security的Filter链中抛出的,默认不会被@ControllerAdvice捕获,需要通过Spring Security专属配置处理,以下是两种实现方案:
方案一:使用Spring Security原生接口(推荐)
通过实现AuthenticationEntryPoint处理401,AccessDeniedHandler处理403,直接在Filter层生成自定义响应,无需转发到Controller。
1. 自定义401处理器
@Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { private static final Logger LOG = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class); private static final String UNAUTHORIZED_MSG = "Token缺失或无效,请重新登录。"; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { LOG.error(UNAUTHORIZED_MSG, authException); // 设置响应头和状态码 response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); // 复用你已有的错误消息生成逻辑 String errorBody = buildErrorResponse(UNAUTHORIZED_MSG, OperationOutcome.IssueType.INVALID); response.getWriter().write(errorBody); } // 复用原ExceptionHandler中的getErrorMessage逻辑 private String buildErrorResponse(String message, OperationOutcome.IssueType issueType) { OperationOutcome operationOutcome = new OperationOutcome(); OperationOutcome.OperationOutcomeIssueComponent component = new OperationOutcome.OperationOutcomeIssueComponent(); component.setSeverity(OperationOutcome.IssueSeverity.ERROR); component.setCode(issueType); component.setDiagnostics(message); operationOutcome.setId(String.valueOf(UUID.randomUUID())); operationOutcome.addIssue(component); return FhirContext.forR4().newJsonParser().encodeResourceToString(operationOutcome); } }
2. 自定义403处理器
@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { private static final Logger LOG = LoggerFactory.getLogger(CustomAccessDeniedHandler.class); private static final String FORBIDDEN_MSG = "权限不足,无法访问该资源。"; @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { LOG.error(FORBIDDEN_MSG, accessDeniedException); response.setStatus(HttpStatus.FORBIDDEN.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); String errorBody = buildErrorResponse(FORBIDDEN_MSG, OperationOutcome.IssueType.FORBIDDEN); response.getWriter().write(errorBody); } // 复用相同的错误消息生成逻辑 private String buildErrorResponse(String message, OperationOutcome.IssueType issueType) { OperationOutcome operationOutcome = new OperationOutcome(); OperationOutcome.OperationOutcomeIssueComponent component = new OperationOutcome.OperationOutcomeIssueComponent(); component.setSeverity(OperationOutcome.IssueSeverity.ERROR); component.setCode(issueType); component.setDiagnostics(message); operationOutcome.setId(String.valueOf(UUID.randomUUID())); operationOutcome.addIssue(component); return FhirContext.forR4().newJsonParser().encodeResourceToString(operationOutcome); } }
3. 注册到Spring Security配置
在你的Security配置类中注入并配置这两个处理器:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationEntryPoint authenticationEntryPoint; @Autowired private CustomAccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http // 其他配置(如JWT过滤器、放行规则等)... .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) // 绑定401处理器 .accessDeniedHandler(accessDeniedHandler); // 绑定403处理器 } }
方案二:通过@ControllerAdvice捕获(需转发异常)
如果希望统一用@ControllerAdvice处理所有异常,需要将Filter层的异常转发到DispatcherServlet:
1. 修改Security配置,转发异常到Controller端点
@Override protected void configure(HttpSecurity http) throws Exception { http // 其他配置... .exceptionHandling() .authenticationEntryPoint((request, response, authException) -> { request.setAttribute("javax.servlet.error.exception", authException); request.getRequestDispatcher("/error/unauthorized").forward(request, response); }) .accessDeniedHandler((request, response, accessDeniedException) -> { request.setAttribute("javax.servlet.error.exception", accessDeniedException); request.getRequestDispatcher("/error/forbidden").forward(request, response); }); }
2. 添加异常转发Controller
@RestController @RequestMapping("/error") public class ErrorForwardController { @Autowired private HealthDataProviderRestExceptionHandler exceptionHandler; @PostMapping("/unauthorized") public ResponseEntity<Object> forwardUnauthorized(HttpServletRequest request) { AuthenticationException ex = (AuthenticationException) request.getAttribute("javax.servlet.error.exception"); return exceptionHandler.handleAuthenticationException(ex); } @PostMapping("/forbidden") public ResponseEntity<Object> forwardForbidden(HttpServletRequest request) { AccessDeniedException ex = (AccessDeniedException) request.getAttribute("javax.servlet.error.exception"); return exceptionHandler.handleAccessDeniedException(ex); } }
3. 在原@ControllerAdvice中添加对应处理方法
@ControllerAdvice public class HealthDataProviderRestExceptionHandler { // 原有的其他异常处理方法... @ExceptionHandler(AuthenticationException.class) public ResponseEntity<Object> handleAuthenticationException(AuthenticationException ex) { LOG.error("Token缺失或无效,请重新登录。", ex); return new ResponseEntity<>(getErrorMessage("Token缺失或无效,请重新登录。", OperationOutcome.IssueType.INVALID), setHeaders(), HttpStatus.UNAUTHORIZED); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<Object> handleAccessDeniedException(AccessDeniedException ex) { LOG.error("权限不足,无法访问该资源。", ex); return new ResponseEntity<>(getErrorMessage("权限不足,无法访问该资源。", OperationOutcome.IssueType.FORBIDDEN), setHeaders(), HttpStatus.FORBIDDEN); } // 原有的setHeaders、getErrorMessage方法... }
内容的提问来源于stack exchange,提问作者Jonathan Hagen
相关产品推荐
相关产品推荐

