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

SpringBoot 3.1.0升级后控制器报错返回401而非500问题求助

问题描述

我近期将SpringBoot从2.7.4版本迁移至3.1.0版本,因部分类已废弃,对Security配置进行了修改。目前所有功能正常,但Spring应用控制器中抛出的任何错误都无法返回给调用方,响应始终为401 Unauthorized。即使是标记为忽略的端点,在发生异常时也会返回401。此前版本中会正常返回500 INTERNAL_SERVER_ERROR,不确定该提供哪些文件,恳请帮助。


相关代码

SecurityConfig类

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Value( "${auth0.audience}" )
    private String audience;

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;


    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/v1/noauth/**", "/actuator/**");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authz) -> authz
                        .requestMatchers("/v1/**").authenticated()
                )
                .oauth2ResourceServer((oauth2) -> oauth2
                        .jwt((jwt) -> jwt
                                .decoder(jwtDecoder())
                        )
                )
                .cors(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable);
        return http.build();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        /*
        By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
        indeed intended for our app. Adding our own validator is easy to do:
        */

        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                JwtDecoders.fromOidcIssuerLocation(issuer);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

        jwtDecoder.setJwtValidator(withAudience);

        return jwtDecoder;
    }
}

Slf4jMDCFilter类(RequestId配置过滤器)

@Data
@EqualsAndHashCode(callSuper = false)
public class Slf4jMDCFilter extends OncePerRequestFilter {
    private final String responseHeader;
    private final String mdcTokenKey;
    private final String requestHeader;

    public Slf4jMDCFilter(String responseHeader, String mdcTokenKey, String requestHeader){
        this.responseHeader = responseHeader;
        this.mdcTokenKey = mdcTokenKey;
        this.requestHeader = requestHeader;
    }

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain)
            throws java.io.IOException, ServletException {
        try {
            final String token;
            if (StringUtils.hasText(requestHeader) && StringUtils.hasText(request.getHeader(requestHeader))) {
                token = URLEncoder.encode(request.getHeader(requestHeader));
            } else {
                token = UUID.randomUUID().toString().toUpperCase();
            }
            MDC.put(mdcTokenKey, token);
            if (StringUtils.hasText(responseHeader)) {
                response.addHeader(responseHeader, token);
            }
            chain.doFilter(request, response);
        } finally {
            MDC.remove(mdcTokenKey);
        }
    }
}

Slf4jMDCFilterConfiguration类(过滤器配置类)

@Configuration
public class Slf4jMDCFilterConfiguration {

    public static final String DEFAULT_RESPONSE_TOKEN_HEADER = "ResponseToken";

    private final String responseHeader = DEFAULT_RESPONSE_TOKEN_HEADER;
    private final String mdcTokenKey = Util.DEFAULT_MDC_UUID_TOKEN_KEY();

    @Value( "${config.slf4jfilter.requestHeader}" )
    private String requestHeader;

    @Bean
    public FilterRegistrationBean servletRegistrationBean() {
        final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        final Slf4jMDCFilter log4jMDCFilterFilter = new Slf4jMDCFilter(responseHeader, mdcTokenKey, requestHeader);
        registrationBean.setFilter(log4jMDCFilterFilter);
        registrationBean.setOrder(2);
        return registrationBean;
    }
}

解决方案

问题根源

Spring Boot 3.x(对应Spring Security 6.x)调整了OAuth2资源服务器的异常处理逻辑:

  1. 默认情况下,所有流经OAuth2过滤器链的请求,未捕获的异常都会被统一转成401响应,覆盖了控制器原本应该返回的500错误
  2. WebSecurity.ignoring()的规则会跳过所有Spring Security过滤器,包括异常处理机制,导致忽略端点的异常也无法被Spring MVC正常处理

修复步骤

1. 调整Security配置,统一异常处理逻辑

替换原有SecurityFilterChain配置,把忽略端点改用permitAll()声明,并添加自定义异常处理规则:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests((authz) -> authz
                    // 用permitAll替代WebSecurity.ignoring(),让请求经过完整过滤器链
                    .requestMatchers("/v1/noauth/**", "/actuator/**").permitAll()
                    .requestMatchers("/v1/**").authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                    .jwt((jwt) -> jwt.decoder(jwtDecoder()))
                    // 仅对认证失败的情况返回401
                    .authenticationEntryPoint((request, response, authException) -> {
                        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
                    })
            )
            .exceptionHandling(exceptions -> exceptions
                    // 权限不足返回403
                    .accessDeniedHandler((request, response, ex) -> {
                        response.sendError(HttpServletResponse.SC_FORBIDDEN, ex.getMessage());
                    })
                    // 已授权端点的异常,交给Spring MVC的错误处理机制处理
                    .defaultAuthenticationEntryPointFor((request, response, ex) -> {
                        request.getRequestDispatcher("/error").forward(request, response);
                    }, request -> request.getRequestURI().startsWith("/v1/"))
            )
            .cors(Customizer.withDefaults())
            .csrf(AbstractHttpConfigurer::disable);
    return http.build();
}

同时删除原有的WebSecurityCustomizer Bean,避免规则冲突。

2. 添加全局异常处理器(可选但推荐)

创建全局异常处理器,统一格式化控制器抛出的异常响应:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                ex.getMessage(),
                request.getRequestURI(),
                LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // 可根据业务需求添加更多特定异常的处理方法
}

// 定义统一的错误响应体
@Data
@AllArgsConstructor
class ErrorResponse {
    private int status;
    private String message;
    private String path;
    private LocalDateTime timestamp;
}

3. 调整过滤器顺序

确保Slf4jMDCFilter在Security过滤器之前执行,避免影响异常处理:

@Bean
public FilterRegistrationBean servletRegistrationBean() {
    final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
    final Slf4jMDCFilter log4jMDCFilterFilter = new Slf4jMDCFilter(responseHeader, mdcTokenKey, requestHeader);
    registrationBean.setFilter(log4jMDCFilterFilter);
    // 设置优先级为最高优先级+1,确保在Security过滤器之前执行
    registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
    return registrationBean;
}

验证场景

修改完成后,测试以下场景确认修复效果:

  • 访问/v1/noauth/**端点并抛出异常,应返回500状态码及自定义错误响应
  • 访问/v1/**端点携带无效Token,应返回401
  • 访问/v1/**端点携带有效Token但控制器抛出异常,应返回500状态码及自定义错误响应

内容的提问来源于stack exchange,提问作者asds_asds

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 00:22:04