Spring Security ProviderManager循环调用及异常处理问题求助
问题背景
使用org.springframework.security:spring-security-core:6.2.3版本,自定义了TokenVerifyFilter过滤器:
class TokenVerifyFilter( private val authenticationManager: AuthenticationManager, private val authenticationEntryPoint: SecurityAuthenticationEntryPoint ) : OncePerRequestFilter() { override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { // ... val authenticate = authenticationManager.authenticate(BearerTokenAuthenticationToken(token)) // ... } }
同时注册了两个AuthenticationProvider:
@Bean fun authenticationManager( http: HttpSecurity, loginAuthenticationProvider: LoginAuthenticationProvider, bearerTokenAuthenticationProvider: BearerTokenAuthenticationProvider ): AuthenticationManager { val authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder::class.java) authenticationManagerBuilder.authenticationProvider(loginAuthenticationProvider).authenticationProvider(bearerTokenAuthenticationProvider) return authenticationManagerBuilder.build() }
当BearerTokenAuthenticationProvider#authenticate抛出继承自AuthenticationException的异常时,系统陷入无限循环。排查发现问题出在ProviderManager的逻辑中:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // ... for (AuthenticationProvider provider : getProviders()) { // ... try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } // 1. catch AuthenticationException catch (AuthenticationException ex) { lastException = ex; } } if (result == null && this.parent != null) { // Allow the parent to try. try { // 2. Trapped in a loop here, Return to the first step. parentResult = this.parent.authenticate(authentication); result = parentResult; } // ... } } }
手动设置authenticationManagerBuilder.parentAuthenticationManager(null)解决了循环问题,但此时抛出的异常会被DispatcherServlet捕获,而非进入后续过滤器处理。现需解决两个问题:
问题解答
1. 未设置父ProviderManager,为何父实例与当前实例相同?
当你通过http.getSharedObject(AuthenticationManagerBuilder::class.java)获取的构建器,默认会关联Spring Security的全局AuthenticationManager作为父容器。而你自定义的AuthenticationManager Bean会被Spring Security自动识别为全局认证管理器,导致构建器在创建实例时,将当前自定义的AuthenticationManager设置为自身的父级,形成循环引用。最终调用parent.authenticate()时,实际还是调用了当前实例的认证逻辑,引发无限循环。
2. 如何让异常正常进入后续过滤器,而非被DispatcherServlet捕获?
核心是不要让认证异常直接逃逸到DispatcherServlet,而是交给Spring Security的异常处理机制处理,同时确保AuthenticationManager无循环引用:
步骤1:修正AuthenticationManager配置,明确设置父级为null
@Bean fun authenticationManager( http: HttpSecurity, loginAuthenticationProvider: LoginAuthenticationProvider, bearerTokenAuthenticationProvider: BearerTokenAuthenticationProvider ): AuthenticationManager { val authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder::class.java) authenticationManagerBuilder .authenticationProvider(loginAuthenticationProvider) .authenticationProvider(bearerTokenAuthenticationProvider) .parentAuthenticationManager(null) // 显式清除父级,避免循环 return authenticationManagerBuilder.build() }
步骤2:修改TokenVerifyFilter,捕获认证异常并交给AuthenticationEntryPoint处理
在过滤器内部捕获AuthenticationException,调用配置好的AuthenticationEntryPoint来处理异常,这样会触发Spring Security的标准异常处理流程,而非让异常直接抛出到DispatcherServlet:
class TokenVerifyFilter( private val authenticationManager: AuthenticationManager, private val authenticationEntryPoint: SecurityAuthenticationEntryPoint ) : OncePerRequestFilter() { override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { try { // 获取token的逻辑 val token = // ... 从request中提取token val authentication = authenticationManager.authenticate(BearerTokenAuthenticationToken(token)) // 将认证结果存入SecurityContext SecurityContextHolder.getContext().authentication = authentication // 继续执行后续过滤器链 filterChain.doFilter(request, response) } catch (ex: AuthenticationException) { // 调用AuthenticationEntryPoint处理认证失败,而非直接抛出异常 authenticationEntryPoint.commence(request, response, ex) } } }
这种方式既避免了无限循环,又能让异常通过Spring Security的机制处理,不会被DispatcherServlet提前捕获。
内容的提问来源于stack exchange,提问作者SageJustus

