
Spring Boot 3.x 升级后,业务异常(如 NullPointerException、SQL 错误)被统一返回 401 Unauthorized,而非预期的 500 Internal Server Error——根本原因在于 /error 端点未被 Spring Security 显式放行,导致错误处理流程被认证拦截器劫持。
spring boot 3.x 升级后,业务异常(如 nullpointerexception、sql 错误)被统一返回 401 unauthorized,而非预期的 500 internal server error——根本原因在于 `/error` 端点未被 spring security 显式放行,导致错误处理流程被认证拦截器劫持。
在 Spring Boot 3.x(配合 Spring Security 6+)中,内置的 BasicErrorController 通过 /error 路径统一捕获并响应所有未处理异常(如 500、404、400)。然而,当该路径未被纳入安全白名单时,请求会先进入 Security 过滤链;由于 /error 默认要求认证,未登录用户访问时触发 AuthenticationEntryPoint,强制返回 401 Unauthorized ——原始异常信息(如 PSQLException: relation "users" does not exist)被彻底掩盖。
你的当前配置中使用了 WebSecurityCustomizer.ignoring() 放行 /v1/noauth/** 和 /actuator/**,但 ignoring() 不适用于 /error:它仅跳过整个 SecurityFilterChain,而 /error 是由 Spring MVC DispatcherServlet 转发的内部请求,必须经由 Security 链完成授权决策。因此,正确做法是在 authorizeHttpRequests() 中显式 permitAll():
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/error").permitAll() // ✅ 关键修复:放行错误处理器
.requestMatchers("/v1/noauth/**", "/actuator/**").permitAll()
.requestMatchers("/v1/**").authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
);
return http.build();
}
⚠️ 注意事项:
- WebSecurityCustomizer.ignoring() 对 /error 无效,因其不参与 MVC 请求分发流程;
- /error 必须出现在 permitAll() 的首位或至少早于 anyRequest().authenticated(),否则匹配规则会被后者覆盖;
- 若同时使用自定义 JWT 过滤器(如 jwtAuthFilter),确保其 doFilterInternal() 中对异常的处理不会提前中断链路(例如避免 response.sendError(500) 后未调用 chain.doFilter());
- 启动时检查日志是否含 Creating filter chain: any request, [org.springframework.security.web.FilterChainProxy],确认 SecurityFilterChain 已成功注册。
此外,验证是否生效的最简方式:手动触发一个控制器异常(如 throw new RuntimeException("test 500")),使用 curl 或 Postman 发起请求,并观察响应状态码与 body 内容。修复后应返回 500 及 JSON 格式的错误详情(如 "status":500,"error":"Internal Server Error","message":"test 500"),而非静默的 401。
最后提醒:Spring Boot 3.x 的安全模型是“默认严格”,任何未显式放行的路径(包括 /error、/actuator/health、甚至静态资源)均受保护。养成在 requestMatchers() 中按最小权限原则逐条声明白名单的习惯,远比依赖 ignoring() 更可靠、可维护。











