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

配置多HttpSecurity实例时触发AlreadyBuiltException问题排查

问题:Spring Security多SecurityFilterChain配置启动报错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。

具体修改步骤:

  1. 移除原有的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();
}
  1. 在需要使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 12:35:13