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

Spring Security 6迁移:AuthenticationManager配置报错及正确配置咨询

问题描述

正在将项目从Spring Boot 2.8迁移至Spring Boot 3,由于Spring Security 6移除了WebSecurityConfigurerAdapter,无法再通过重写configure(AuthenticationManagerBuilder)方法配置认证。

原Spring Boot 2.x配置代码:

@SuppressWarnings("deprecation")
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    if (!this.dmeAppSettings.isDevMode()) { // production
        auth.authenticationEventPublisher(authEventPub)
            .userDetailsService(userDetailsService())
            .passwordEncoder(pwdEncoder);
    } else {
        log.info("Configured to use in memory user accounts for authentication");
        auth.authenticationEventPublisher(authEventPub)
            .userDetailsService(userDetailsService())
            .and()
            .inMemoryAuthentication()
            .passwordEncoder(
                org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
            .withUser("user")
            .password("password")
            .roles("USER")
            .and()
            .passwordEncoder(
                org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
            .withUser("admin")
            .password("password")
            .roles(ADMIN_ONLY_ROLES)
            .and()
            .passwordEncoder(
                org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
            .withUser("sysadmin")
            .password("password")
            .roles(SYS_ADMIN_ROLES)
            .and()
            .passwordEncoder(
                org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
            .withUser("fsadmin")
            .password("password")
            .roles(FS_ADMIN_ROLES)
            .and()
            .passwordEncoder(
                org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
            .withUser("centcomadmin")
            .password("password")
            .roles(CENTCOM_ADMIN_ROLES);
    }
}

@Bean
public UserDetailsService userDetailsService() {
    if (this.dmeAppSettings.isDevMode()) { // Dev mode.
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
                List<GrantedAuthority> auths = new ArrayList<>();
                if (userId.equalsIgnoreCase("ADMIN")) {
                    for (String admin : ADMIN_ONLY_ROLES) {
                        auths.add(new SimpleGrantedAuthority("ROLE_" + admin));
                    }
                } else if (userId.equalsIgnoreCase("SYSADMIN")) {
                    for (String admin : SYS_ADMIN_ROLES) {
                        auths.add(new SimpleGrantedAuthority("ROLE_" + admin));
                    }
                } else if (userId.equalsIgnoreCase("FSSADMIN")) {
                    for (String admin : FS_ADMIN_ROLES) {
                        auths.add(new SimpleGrantedAuthority("ROLE_" + admin));
                    }
                } else if (userId.equalsIgnoreCase("CENTCOMADMIN")) {
                    for (String admin : CENTCOM_ADMIN_ROLES) {
                        auths.add(new SimpleGrantedAuthority("ROLE_" + admin));
                    }
                }
                User result = new User(userId, "password", true, true, true, true, auths);
                return result;
            }
        };
    } else { // Production mode.
        return new DmeappUserService(dmeAppSettings, userServiceDb);
    }
}

核心需求:

  • 设置自定义AuthenticationEventPublisher
  • 开发环境使用内存认证
  • 生产环境使用数据库用户服务

尝试创建AuthenticationManager Bean时触发异常,代码及异常栈如下:

@Bean
public AuthenticationManager authenticationManager(
    AuthenticationManagerBuilder auth)
    throws Exception {
    if (!this.dmeAppSettings.isDevMode()) { // production
        auth.authenticationEventPublisher(authEventPub)
            .userDetailsService(userDetailsService())
            .passwordEncoder(pwdEncoder);
    } else {
        log.info("Configured to use in memory user accounts for authentication");
        auth.authenticationEventPublisher(authEventPub)
            .userDetailsService(userDetailsService())
            .and()
            .inMemoryAuthentication()
            .passwordEncoder(
                org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
            .withUser("user").password("password").roles("USER")
            .and()
            .withUser("admin").password("password").roles(ADMIN_ONLY_ROLES)
            .and()
            .withUser("sysadmin").password("password").roles(SYS_ADMIN_ROLES)
            .and()
            .withUser("fsadmin").password("password").roles(FS_ADMIN_ROLES)
            .and()
            .withUser("centcomadmin").password("password").roles(CENTCOM_ADMIN_ROLES);
    }
    return auth.build();
}

异常栈:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'authenticationManager' defined in class path resource [com/missionessential/dmeapp/config/DmeappSecurityConfig.class]: Failed to instantiate [org.springframework.security.authentication.AuthenticationManager]: Factory method 'authenticationManager' threw exception with message: Cannot apply org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer@520a8ea1 to already built object
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:652)
        at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:640)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1324)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1161)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:961)
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:915)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:584)
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:432)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1302)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1291)
        at com.missionessential.dmeapp.DmeappApplication.main(DmeappApplication.java:63)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.authentication.AuthenticationManager]: Factory method 'authenticationManager' threw exception with message: Cannot apply org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer@520a8ea1 to already built object
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:171)
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:648)
        ... 19 common frames omitted
Caused by: java.lang.IllegalStateException: Cannot apply org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer@520a8ea1 to already built object
        at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.add(AbstractConfiguredSecurityBuilder.java:182)
        at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.apply(AbstractConfiguredSecurityBuilder.java:125)
        at org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder.apply(AuthenticationManagerBuilder.java:280)
        at org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder.userDetailsService(AuthenticationManagerBuilder.java:182)
        at org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder.userDetailsService(AuthenticationConfiguration.java:298)
        at com.missionessential.dmeapp.config.DmeappSecurityConfig.authenticationManager(DmeappSecurityConfig.java:230)
        at com.missionessential.dmeapp.config.DmeappSecurityConfig$$SpringCGLIB$$0.CGLIB$authenticationManager$0(<generated>)
        at com.missionessential.dmeapp.config.DmeappSecurityConfig$$SpringCGLIB$$2.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:257)
        at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331)
        at com.missionessential.dmeapp.config.DmeappSecurityConfig$$SpringCGLIB$$0.authenticationManager(<generated>)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:139)
        ... 20 common frames omitted
错误原因分析

直接注入的AuthenticationManagerBuilder是Spring Security自动配置的实例,它可能已被提前构建,此时再调用userDetailsService()等方法会触发"Cannot apply ... to already built object"异常。另外,开发环境中同时配置userDetailsService()和inMemoryAuthentication会造成冲突,因为内存认证本身会提供自己的UserDetailsService,无需额外配置。

正确配置方案

1. 保留原UserDetailsService Bean

原有的userDetailsService() Bean无需修改,它会根据环境返回对应实现。

2. 重构AuthenticationManager配置

通过AuthenticationConfiguration获取新的AuthenticationManagerBuilder实例,分别针对生产/开发环境配置,同时统一设置自定义事件发布器:

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig,
                                                   AuthenticationEventPublisher authEventPub,
                                                   PasswordEncoder pwdEncoder,
                                                   DmeAppSettings dmeAppSettings,
                                                   UserDetailsService userDetailsService) throws Exception {
    AuthenticationManagerBuilder authBuilder = authConfig.getAuthenticationManagerBuilder();
    
    // 全局设置自定义事件发布器
    authBuilder.authenticationEventPublisher(authEventPub);
    
    if (!dmeAppSettings.isDevMode()) {
        // 生产环境:绑定数据库用户服务与密码编码器
        authBuilder.userDetailsService(userDetailsService)
                   .passwordEncoder(pwdEncoder);
    } else {
        log.info("Configured to use in memory user accounts for authentication");
        // 开发环境:配置内存认证,无需额外绑定UserDetailsService
        authBuilder.inMemoryAuthentication()
                   .passwordEncoder(NoOpPasswordEncoder.getInstance())
                   .withUser("user")
                   .password("password")
                   .roles("USER")
                   .and()
                   .withUser("admin")
                   .password("password")
                   .roles(ADMIN_ONLY_ROLES)
                   .and()
                   .withUser("sysadmin")
                   .password("password")
                   .roles(SYS_ADMIN_ROLES)
                   .and()
                   .withUser("fsadmin")
                   .password("password")
                   .roles(FS_ADMIN_ROLES)
                   .and()
                   .withUser("centcomadmin")
                   .password("password")
                   .roles(CENTCOM_ADMIN_ROLES);
    }
    
    return authBuilder.build();
}

关键调整点

  • 通过AuthenticationConfiguration.getAuthenticationManagerBuilder()获取全新的构建器实例,避免使用容器中已构建的实例
  • 开发环境移除多余的userDetailsService()配置,inMemoryAuthentication会自动注册对应的UserDetailsService
  • 全局统一设置AuthenticationEventPublisher,无需重复配置
  • 直接注入已有的UserDetailsService Bean,避免重复创建实例

补充说明

  • NoOpPasswordEncoder仅用于开发环境,生产环境必须使用安全的密码编码器(如BCryptPasswordEncoder)
  • 确保DmeAppSettings、AuthenticationEventPublisher等Bean已正确注册到Spring容器中

内容的提问来源于stack exchange,提问作者Ken

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 00:50:53