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

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]

求问这是什么原因?代码里缺了什么配置才能正确捕获这些异常?


解决方案

核心原因

  1. 异常类型不匹配:你的UnauthorizedException没有继承Spring Security的AuthenticationException,导致AuthenticationEntryPoint和AuthenticationFailureHandler都不会处理该异常,最终异常被抛到容器层,触发Spring Boot默认错误处理。
  2. 处理器触发场景错误: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
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 08:50:27