配置多HttpSecurity实例时触发AlreadyBuiltException问题排查
我使用Spring v5.3.28和Spring Security v5.8.4,因WebSecurityConfigurerAdapter已废弃,改用无继承的方式配置Spring Security。项目编译成功,但启动时抛出AlreadyBuiltException,提示对象已被构建。
我的Security配置代码:
@Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfiguration { private static final String SIGNIN = "/signin"; @Autowired ApiCustomAuthenticationProviderImpl apiCustomAuthenticationProvider; @Autowired CustomAuthenticationProviderImpl customAuthenticationProvider; @Autowired LdapAuthenticationProviderImpl ldapAuthenticationProvider; @Autowired SautenticaAuthenticationProviderImpl sautenticaAuthenticationProvider; @Autowired ConfigurationService configurationService; @Bean protected UserDetailsService userDetailsService () { return new AutenticacionServiceImpl(); } @Bean public EvaluationContextExtension securityExtension () { return new SecurityEvaluationContextExtension(); } @Bean public AuditorAware<Usuario> auditorAware () { return new SpringSecurityAuditorAware(); } public AuthenticationManager getAuthenticationManager (HttpSecurity http) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); final String authMode = configurationService.findByCode(Constants.CT_AUTH_MODE); if (StringUtils.isNotEmpty(authMode) && Constants.CT_LDAP.equalsIgnoreCase(authMode)) { authenticationManagerBuilder.authenticationProvider(ldapAuthenticationProvider); } else if (StringUtils.isNotEmpty(authMode) && Constants.CT_SAUTENTICA.equalsIgnoreCase(authMode)) { authenticationManagerBuilder.authenticationProvider(sautenticaAuthenticationProvider); } else { authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider); } authenticationManagerBuilder.eraseCredentials(false); return authenticationManagerBuilder.build(); } @Bean @Order(1) public SecurityFilterChain apiSecurityFilterChain (HttpSecurity http) throws Exception { try { http.securityMatcher("/api-ws/**"); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeHttpRequests().requestMatchers("/api-ws/**").authenticated().and().httpBasic().and().csrf().disable() .addFilterAfter(new ApiCustomBasicAuthenticationFilter(), BasicAuthenticationFilter.class); http.authenticationProvider(apiCustomAuthenticationProvider); } catch (final Exception e) { throw new SecurityException(e); } return http.build(); } @Bean @Order(2) public SecurityFilterChain wsSecurityFilterChain (HttpSecurity http) throws Exception { try { http.securityMatcher("/ws/**"); http.authorizeHttpRequests().requestMatchers("/ws/patient/**", "/meet-ws/case/**").authenticated(); http.httpBasic().and().addFilterAt(new CustomBasicAuthenticationFilter(getAuthenticationManager(http)), BasicAuthenticationFilter.class); http.headers().frameOptions().disable(); } catch (final Exception e) { throw new SecurityException(e); } return http.build(); } @Bean public WebSecurityCustomizer webSecurityCustomizer () { return web -> web.ignoring().requestMatchers("/ws/login**", "/ws/new/xml**"); } @Bean public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { try { http.authorizeHttpRequests().requestMatchers("/favicon.ico", "/resources/**", "/health**", "/signin**", "/authenticate**").permitAll().anyRequest().authenticated() .and().authenticationManager(getAuthenticationManager(http)).formLogin().loginPage(SIGNIN).permitAll().loginProcessingUrl("/authenticate").permitAll() .failureUrl("/signin?error=1").successHandler(new CustomAuthenticationSuccessHandler()).and().sessionManagement().invalidSessionUrl(SIGNIN).and() .exceptionHandling().accessDeniedHandler(new AccessDeniedHandler()).and().headers().frameOptions().disable(); http.logout().logoutUrl("/logout").logoutSuccessUrl(SIGNIN).permitAll(); } catch (final Exception e) { throw new SecurityException(e); } return http.build(); } }
启动时抛出的核心异常片段:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Unsatisfied dependency expressed through method 'setFilterChains' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'wsSecurityFilterChain' defined in es.mycompany.myapp.SecurityConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'wsSecurityFilterChain' threw exception; nested exception is org.springframework.security.config.annotation.AlreadyBuiltException: This object has already been built ...(省略冗余栈帧) Caused by: org.springframework.security.config.annotation.AlreadyBuiltException: This object has already been built at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:41) at org.springframework.security.config.annotation.web.builders.HttpSecurity.beforeConfigure(HttpSecurity.java:3225) at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:308) at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38) at es.mycompany.myapp.SecurityConfiguration.wsSecurityFilterChain(SecurityConfiguration.java:152) ...(省略冗余栈帧)
我猜测是http.build()被调用两次导致,但写法和官方文档一致,想知道问题原因和解决办法。
问题出在getAuthenticationManager(HttpSecurity http)方法中调用的authenticationManagerBuilder.build()——这个操作会触发当前HttpSecurity实例的提前构建,而后续在wsSecurityFilterChain和securityFilterChain方法中还会调用http.build(),导致同一个HttpSecurity实例被重复构建,触发AlreadyBuiltException。
Spring Security的HttpSecurity是AbstractConfiguredSecurityBuilder的实现类,它的build()方法只能被调用一次,一旦调用后实例就会被标记为已构建,再次调用就会抛出异常。
不要从HttpSecurity中获取AuthenticationManagerBuilder来构建AuthenticationManager,而是单独创建全局的AuthenticationManager Bean,让多个SecurityFilterChain共享这个全局实例,避免提前构建HttpSecurity。
具体修改步骤:
- 移除原有的
getAuthenticationManager(HttpSecurity http)方法,替换为全局AuthenticationManager Bean:
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = authConfig.getAuthenticationManagerBuilder(); final String authMode = configurationService.findByCode(Constants.CT_AUTH_MODE); if (StringUtils.isNotEmpty(authMode) && Constants.CT_LDAP.equalsIgnoreCase(authMode)) { authenticationManagerBuilder.authenticationProvider(ldapAuthenticationProvider); } else if (StringUtils.isNotEmpty(authMode) && Constants.CT_SAUTENTICA.equalsIgnoreCase(authMode)) { authenticationManagerBuilder.authenticationProvider(sautenticaAuthenticationProvider); } else { authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider); } authenticationManagerBuilder.eraseCredentials(false); return authenticationManagerBuilder.build(); }
- 在需要使用AuthenticationManager的SecurityFilterChain方法中,直接注入全局Bean,不再从HttpSecurity中获取:
修改wsSecurityFilterChain方法:
@Bean @Order(2) public SecurityFilterChain wsSecurityFilterChain (HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { try { http.securityMatcher("/ws/**"); http.authorizeHttpRequests().requestMatchers("/ws/patient/**", "/meet-ws/case/**").authenticated(); http.httpBasic().and().addFilterAt(new CustomBasicAuthenticationFilter(authenticationManager), BasicAuthenticationFilter.class); http.headers().frameOptions().disable(); } catch (final Exception e) { throw new SecurityException(e); } return http.build(); }
修改securityFilterChain方法:
@Bean public SecurityFilterChain securityFilterChain (HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { try { http.authorizeHttpRequests().requestMatchers("/favicon.ico", "/resources/**", "/health**", "/signin**", "/authenticate**").permitAll().anyRequest().authenticated() .and().authenticationManager(authenticationManager).formLogin().loginPage(SIGNIN).permitAll().loginProcessingUrl("/authenticate").permitAll() .failureUrl("/signin?error=1").successHandler(new CustomAuthenticationSuccessHandler()).and().sessionManagement().invalidSessionUrl(SIGNIN).and() .exceptionHandling().accessDeniedHandler(new AccessDeniedHandler()).and().headers().frameOptions().disable(); http.logout().logoutUrl("/logout").logoutSuccessUrl(SIGNIN).permitAll(); } catch (final Exception e) { throw new SecurityException(e); } return http.build(); }
修改后,全局AuthenticationManager只构建一次,每个SecurityFilterChain对应的HttpSecurity都是独立配置并构建,不会出现重复构建的问题。
内容的提问来源于stack exchange,提问作者diminuta

