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,无需重复配置 - 直接注入已有的
UserDetailsServiceBean,避免重复创建实例
补充说明
NoOpPasswordEncoder仅用于开发环境,生产环境必须使用安全的密码编码器(如BCryptPasswordEncoder)- 确保
DmeAppSettings、AuthenticationEventPublisher等Bean已正确注册到Spring容器中
内容的提问来源于stack exchange,提问作者Ken
相关产品推荐
相关产品推荐

