
spring boot中rest接口抛出404、500等业务异常却返回403,本质是spring security拦截了/error路径导致认证上下文丢失,而非控制器逻辑错误;需显式放行错误端点并合理配置异常处理器。
spring boot中rest接口抛出404、500等业务异常却返回403,本质是spring security拦截了/error路径导致认证上下文丢失,而非控制器逻辑错误;需显式放行错误端点并合理配置异常处理器。
在Spring Boot + Spring Security项目中,一个看似“奇怪”的现象频繁出现:明明在@RestController方法中主动抛出@ResponseStatus(HttpStatus.NOT_FOUND)标注的自定义异常(如NotFoundException),客户端收到的却是403 Forbidden,而非预期的404 Not Found。日志显示DispatcherServlet已正确完成404 NOT_FOUND,但随后请求被重定向至/error,并在该路径上触发Http403ForbiddenEntryPoint——这揭示了问题的核心:Spring Security默认将/error视为受保护资源,而错误转发时原认证上下文已被清空,导致匿名访问被拒绝。
? 问题链路还原
- 控制器抛出NotFoundException → DispatcherServlet捕获并标记响应状态为404;
- 无全局@ControllerAdvice捕获该异常 → Spring Boot默认启用BasicErrorController,自动重定向至/error?path=xxx;
- /error路径未在HttpSecurity中显式放行 → AnonymousAuthenticationFilter将SecurityContext设为匿名;
- BasicErrorController#error()被调用,但因当前用户无权限访问/error → Http403ForbiddenEntryPoint介入,强制返回403。
✅ 关键证据:日志中连续出现Set SecurityContextHolder to anonymous SecurityContext和Pre-authenticated entry point called. Rejecting access。
✅ 正确解决方案
1. 显式放行 /error 端点(必要基础)
在SecurityFilterChain配置中,必须将/error加入permitAll()白名单:
@Bean
fun securityFilterChain(http: HttpSecurity, authenticationManager: AuthenticationManager): SecurityFilterChain {
http.authorizeHttpRequests()
.requestMatchers("/").permitAll()
.requestMatchers("/login-state", "/verify-code", "/oauth2/**").permitAll()
.requestMatchers("/error").permitAll() // ← 关键:放行错误端点
.anyRequest().authenticated()
// ... 其他配置(JWT Filter、CORS、CSRF禁用等)
return http.build()
}
⚠️ 注意:仅放行/error仍不够——此时BasicErrorController会返回HTML格式错误页(含status=403),而非JSON。需进一步配置以支持RESTful错误响应。
2. 配置RESTful错误响应格式
在application.yml中声明错误响应为JSON:
server:
error:
include-message: always
include-binding-errors: always
spring:
web:
resources:
add-mappings: false # 避免静态资源干扰
更推荐方式:自定义全局异常处理器,彻底绕过/error机制:
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException::class)
fun handleNotFound(ex: NotFoundException, request: HttpServletRequest): ResponseEntity<errorresponse> {
val error = ErrorResponse(
timestamp = Instant.now(),
status = HttpStatus.NOT_FOUND.value(),
error = "Not Found",
message = ex.message ?: "Resource not found",
path = request.requestURI
)
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error)
}
@ExceptionHandler(Exception::class)
fun handleGeneric(ex: Exception, request: HttpServletRequest): ResponseEntity<errorresponse> {
logger.error("Unhandled exception for ${request.requestURI}", ex)
val error = ErrorResponse(
timestamp = Instant.now(),
status = HttpStatus.INTERNAL_SERVER_ERROR.value(),
error = "Internal Server Error",
message = "An unexpected error occurred",
path = request.requestURI
)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error)
}
}
data class ErrorResponse(
val timestamp: Instant,
val status: Int,
val error: String,
val message: String,
val path: String
)</errorresponse></errorresponse>
3. Spring Security 6+ 配置升级要点(适配Spring Boot 3.x)
你当前配置中混用了旧版注解(如@EnableGlobalMethodSecurity)与新版HttpSecurity DSL,易引发冲突:
- ❌ 移除已废弃的@EnableGlobalMethodSecurity;
- ✅ 启用@EnableMethodSecurity(Spring Security 6.0+ 推荐):
@Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true, proxyTargetClass = true) // 替代旧注解 class BasicWebSecurityConfigAdapter { /* ... */ } - ⚠️ requestMatchers()替代已弃用的antMatchers()(你的代码中已正确使用,值得肯定)。
? 总结:三步规避403陷阱
| 步骤 | 操作 | 目的 |
|---|---|---|
| 1. 放行错误端点 | requestMatchers("/error").permitAll() | 防止/error被Security拦截导致403 |
| 2. 统一异常处理 | @RestControllerAdvice + @ExceptionHandler | 主动控制HTTP状态码与响应体,避免依赖BasicErrorController |
| 3. 升级安全配置 | 用@EnableMethodSecurity替代@EnableGlobalMethodSecurity | 兼容Spring Security 6+,消除注解冲突风险 |
? 最佳实践建议:永远不要依赖默认/error行为处理REST API异常。@RestControllerAdvice是解耦、可控、可测试的黄金方案——它让异常处理逻辑与安全配置完全分离,既保障状态码准确性,又提升API健壮性。











