Spring Boot使用AuthenticationManager引发递归与StackOverflowError问题
o.s.s.a.dao.DaoAuthenticationProvider : Failed to find user 'admin'
o.s.s.authentication.ProviderManager : Authenticating request with DaoAuthenticationProvider (1/1)
o.s.s.a.dao.DaoAuthenticationProvider : Failed to find user 'admin'
o.s.s.authentication.ProviderManager : Authenticating request with DaoAuthenticationProvider (1/1)
...
- 用户名正确但密码错误时触发递归调用,最终抛出`StackOverflowError`:
...
o.s.s.authentication.ProviderManager : Authenticating request with DaoAuthenticationProvider (1/1)
o.s.s.a.dao.DaoAuthenticationProvider : Failed to authenticate since password does not match stored value
...
java.lang.StackOverflowError: null
at java.base/java.util.stream.IntPipeline.forEachWithCancel(IntPipeline.java:161) ~[na:na]
...
## 问题代码 完整的Spring Security配置类: ```java @Configuration @EnableWebSecurity public class WebSecurityConfig { @Bean public SecurityFilterChain defaultFilterChain(HttpSecurity http, AuthenticationManager authManager) throws Exception { return http .authorizeRequests(auth -> auth .mvcMatchers("/").permitAll() .mvcMatchers("/**").access("hasRole('USER')")) .formLogin(FormLoginConfigurer::permitAll) .authenticationManager(authManager) .build(); } @Bean public AuthenticationManager authManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); authManagerBuilder.inMemoryAuthentication() .withUser("user") .password("{noop}password") .roles("USER"); return authManagerBuilder.build(); } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }
核心疑惑
为何直接使用InMemoryUserDetailsManager Bean可以正常工作,但通过AuthenticationManagerBuilder构建AuthenticationManager就会出现认证失败时的递归/循环问题?
问题根源是自定义AuthenticationManager与Spring Security默认流程形成了递归调用链:
- 当你通过
http.authenticationManager(authManager)把自定义的AuthenticationManager注入到安全过滤器链时,这个管理器内部的ProviderManager在认证失败时,会默认尝试委托给父级AuthenticationManager。 - 而你通过
http.getSharedObject(AuthenticationManagerBuilder.class)构建的管理器,其默认父级就是Spring Security自动配置的全局管理器,这个全局管理器又会回过来调用你自定义的管理器,形成无限递归。 - 认证失败场景下,这个递归会不断触发,要么导致日志无限循环,要么最终撑爆栈内存抛出
StackOverflowError。
方案一:直接配置UserDetailsService(推荐)
放弃自定义AuthenticationManager,直接注册InMemoryUserDetailsService,让Spring Security自动构建正确的AuthenticationManager,避免手动构建带来的递归问题:
@Configuration @EnableWebSecurity public class WebSecurityConfig { @Bean public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception { return http .authorizeRequests(auth -> auth .mvcMatchers("/").permitAll() .mvcMatchers("/**").access("hasRole('USER')")) .formLogin(FormLoginConfigurer::permitAll) .build(); } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withUsername("user") .password("{noop}password") .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }
方案二:自定义AuthenticationManager时切断递归链
如果必须手动构建AuthenticationManager,在构建时显式设置父级管理器为null,打破递归调用链:
@Bean public AuthenticationManager authManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); authManagerBuilder.inMemoryAuthentication() .withUser("user") .password("{noop}password") .roles("USER"); // 显式设置父级AuthenticationManager为null,避免递归调用 return authManagerBuilder.parentAuthenticationManager(null).build(); }
补充说明
直接使用InMemoryUserDetailsManager不会出问题,是因为Spring Security会自动为它生成一个没有父级的AuthenticationManager,从根源上避免了递归调用的可能。
内容的提问来源于stack exchange,提问作者Slevin

