Spring Security认证异常无法捕获问题求助
问题:Spring Security认证异常无法被自定义处理器捕获
我正在学习用Spring Boot开发REST API,其他功能都正常,但卡在了Spring Security认证异常的处理上。
我用@ControllerAdvice写了全局异常处理器,能捕获应用里的其他异常,但认证阶段抛出的异常在DispatcherServlet之前就触发了,@ControllerAdvice根本抓不到。
于是我尝试配置AuthenticationEntryPoint来处理:
- 在Kotlin的SecurityFilterChain里实现了
authEntryPoint()方法; - 在SecurityFilterChain配置中添加了
.exceptionHandling { it.authenticationEntryPoint(authEntryPoint()) }。
SecurityFilterChain代码
@Configuration @EnableWebSecurity class SecurityConfig( private val certProperties: CertProperties, @Qualifier("handlerExceptionResolver") private val resolver: HandlerExceptionResolver ) { @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { return http .csrf().disable() .authorizeRequests { it.antMatchers("/v1/countries/**").authenticated() } .exceptionHandling { it.authenticationEntryPoint(authEntryPoint()) } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .oauth2ResourceServer { it.jwt() } .httpBasic(Customizer.withDefaults()) .build() } private fun authEntryPoint() = AuthenticationEntryPoint { request, response, authException -> // 此代码从未被调用 resolver.resolveException(request, response, null, authException) } @Bean fun jwtDecoder() = JwtDecoder { token -> val signedJwt = SignedJWT.parse(token) val isValid = signedJwt.verify(ECDSAVerifier(certProperties.publicKey)) if (!isValid) throw UnauthorizedException(type = "AUTH_INVALID") Jwt( token, signedJwt.jwtClaimsSet.issueTime.toInstant(), signedJwt.jwtClaimsSet.expirationTime.toInstant(), signedJwt.header.toJSONObject(), signedJwt.jwtClaimsSet.toJSONObject() ) } }
RestExceptionHandler代码
@ControllerAdvice class RestExceptionHandler { @ExceptionHandler(value = [AuthenticationException::class]) fun handleApiException(ex: AuthenticationException): ResponseEntity<HttpException.Body> { // 此代码从未被调用 val exception = UnauthorizedException(title = title, detail = detail) return ResponseEntity(exception.body, exception.status) } }
但当JWT无效触发jwtDecoder()里的异常时,authEntryPoint()和handleApiException()都没被调用,返回的是Spring Boot默认异常,日志里的堆栈信息如下:
异常堆栈
com.learnspring.exception.UnauthorizedException: null at com.learnspring.config.SecurityConfig.jwtDecoder$lambda-6(SecurityConfig.kt:80) ~[main/:na] at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.getJwt(JwtAuthenticationProvider.java:97) ~[spring-security-oauth2-resource-server-5.7.3.jar:5.7.3] at org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider.authenticate(JwtAuthenticationProvider.java:88) ~[spring-security-oauth2-resource-server-5.7.3.jar:5.7.3] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) ~[spring-security-core-5.7.3.jar:5.7.3] at org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter.doFilterInternal(BearerTokenAuthenticationFilter.java:134) ~[spring-security-oauth2-resource-server-5.7.3.jar:5.7.3] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:112) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:82) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:221) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:186) ~[spring-security-web-5.7.3.jar:5.7.3] at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
求问这是什么原因?代码里缺了什么配置才能正确捕获这些异常?
解决方案
核心原因
- 异常类型不匹配:你的
UnauthorizedException没有继承Spring Security的AuthenticationException,导致AuthenticationEntryPoint和AuthenticationFailureHandler都不会处理该异常,最终异常被抛到容器层,触发Spring Boot默认错误处理。 - 处理器触发场景错误:
AuthenticationEntryPoint仅处理未携带Token访问受保护资源的场景,而Token无效/认证失败的场景会触发AuthenticationFailureHandler,这也是你的authEntryPoint()未被调用的原因。
修复步骤
1. 让自定义异常继承AuthenticationException
修改UnauthorizedException的定义,确保它继承自Spring Security的认证异常类:
class UnauthorizedException( type: String, title: String? = null, detail: String? = null ) : AuthenticationException("Authentication failed") { val body = HttpException.Body(type, title, detail) val status = HttpStatus.UNAUTHORIZED }
2. 配置BearerTokenAuthenticationFilter的失败处理器
在SecurityConfig中为BearerTokenAuthenticationFilter配置AuthenticationFailureHandler,处理Token认证失败的异常:
.oauth2ResourceServer { oauth2 -> oauth2.jwt() oauth2.bearerTokenAuthenticationFilter { filter -> filter.setAuthenticationFailureHandler { request, response, exception -> resolver.resolveException(request, response, null, exception) } } }
3. 更新全局异常处理器
确保RestExceptionHandler能捕获自定义异常和Spring Security的认证异常:
@ControllerAdvice class RestExceptionHandler { @ExceptionHandler(value = [AuthenticationException::class]) fun handleAuthenticationException(ex: AuthenticationException): ResponseEntity<HttpException.Body> { val unauthorizedEx = if (ex is UnauthorizedException) { ex } else { UnauthorizedException(type = "AUTH_INVALID", detail = ex.message) } return ResponseEntity(unauthorizedEx.body, unauthorizedEx.status) } }
4. 可选:配置访问拒绝处理器
如果需要处理权限不足的异常,可以在exceptionHandling中添加accessDeniedHandler:
.exceptionHandling { it.authenticationEntryPoint(authEntryPoint()) it.accessDeniedHandler { request, response, ex -> resolver.resolveException(request, response, null, ex) } }
完整修改后的SecurityConfig示例
@Configuration @EnableWebSecurity class SecurityConfig( private val certProperties: CertProperties, @Qualifier("handlerExceptionResolver") private val resolver: HandlerExceptionResolver ) { @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { return http .csrf().disable() .authorizeRequests { it.antMatchers("/v1/countries/**").authenticated() } .exceptionHandling { it.authenticationEntryPoint(authEntryPoint()) it.accessDeniedHandler { request, response, ex -> resolver.resolveException(request, response, null, ex) } } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .oauth2ResourceServer { oauth2 -> oauth2.jwt() oauth2.bearerTokenAuthenticationFilter { filter -> filter.setAuthenticationFailureHandler { request
相关产品推荐
相关产品推荐

