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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 20:24:55