UserDetailService是在什么环节生效的,为什么自定义之后就能被识别
本文介绍了Spring Security认证管理器的配置过程。主要内容包括:1. 框架默认会生成基于内存的用户和密码,当检测到用户未配置相关组件时通过UserDetailsServiceAutoConfiguration自动创建InMemoryUserDetailsManager;2. 认证过程由过滤器链拦截后委托给AuthenticationManager处理,其核心通过
今天搞懂3个问题:
1.为什么引入springsecurity后我们发现,框架会为我们生成一个基于内存的用户和密码?
2.如何实现基于数据库的用户的认证和授权?
3.为什么配置一个UserDetailService的Bean就能够起作用?
认证过程概述
认证请求被过滤器链拦截后,需要将认证请求委托给AuthenticationManager认证管理器 也就是说过滤器链中的过滤器是可以获取到对应的认证管理器的。这个是因为DefaultSecurityFilterChain过滤器链是由HttpSecurity 构建的。而HttpSecurity框架初始化的时候就创建了一个全局的认证管理器,代码如下:
HttpSecurityConfiguration负责配置HttpSecurity的Bean实例,将其配置了prototype类型。也就是每一个过滤器链都是一个独立全新的HttpSecurity。
@Bean(HTTPSECURITY_BEAN_NAME) @Scope("prototype") HttpSecurity httpSecurity() throws Exception { LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context); AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder( this.objectPostProcessor, passwordEncoder); authenticationBuilder.parentAuthenticationManager(authenticationManager()); authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher()); HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects()); WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter(); webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); // @formatter:off http .csrf(withDefaults()) .addFilter(webAsyncManagerIntegrationFilter) .exceptionHandling(withDefaults()) .headers(withDefaults()) .sessionManagement(withDefaults()) .securityContext(withDefaults()) .requestCache(withDefaults()) .anonymous(withDefaults()) .servletApi(withDefaults()) .apply(new DefaultLoginPageConfigurer<>()); http.logout(withDefaults()); // @formatter:on applyCorsIfAvailable(http); applyDefaultConfigurers(http); return http; }
上面的代码中有一句是设置了全局过滤器,通过调用authenticationManager() 方法,将创建的全局管理器保存到authenticationBuilder中。
authenticationBuilder.parentAuthenticationManager(authenticationManager());
//注入AuthenticationConfiguration @Autowired void setAuthenticationConfiguration(AuthenticationConfiguration authenticationConfiguration) { this.authenticationConfiguration = authenticationConfiguration; } // 调用authenticationConfiguration的getAuthenticationManager创建认证管理器 private AuthenticationManager authenticationManager() throws Exception { return this.authenticationConfiguration.getAuthenticationManager(); }
从上面的代码中我们发现全局管理器底层是由AuthenticationConfiguration 创建的
AuthenticationConfiguration
AuthenticationConfiguration是由EnableGlobalAuthentication注解import生效的。从名称来看EnableGlobalAuthentication是为了使全局的配置生效的。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(AuthenticationConfiguration.class) public @interface EnableGlobalAuthentication { }
AuthenticationConfiguration中配置了一个重要的bean
@Bean public AuthenticationManagerBuilder authenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor, ApplicationContext context) { LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context); AuthenticationEventPublisher authenticationEventPublisher = getAuthenticationEventPublisher(context); DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder( objectPostProcessor, defaultPasswordEncoder); if (authenticationEventPublisher != null) { result.authenticationEventPublisher(authenticationEventPublisher); } return result; }
同时还提供了上面提到的创建AuthenticationManager的方法
public AuthenticationManager getAuthenticationManager() throws Exception { if (this.authenticationManagerInitialized) { return this.authenticationManager; } AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class); if (this.buildingAuthenticationManager.getAndSet(true)) { return new AuthenticationManagerDelegator(authBuilder); } for (GlobalAuthenticationConfigurerAdapter config : this.globalAuthConfigurers) { authBuilder.apply(config); } this.authenticationManager = authBuilder.build(); if (this.authenticationManager == null) { this.authenticationManager = getAuthenticationManagerBean(); } this.authenticationManagerInitialized = true; return this.authenticationManager; }
从上面的代码可以看出 创建AuthenticationManager的核心逻辑是
- 1.AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class); 获取容器中的AuthenticationManagerBuilder
- 2.authBuilder.apply(config); 遍历 globalAuthConfigurers中所有的全局配置,然后应用到authBuilder上
-
- this.authenticationManager = authBuilder.build(); 执行构建方法
那么globalAuthConfigurers配置是哪里获取到的呢?AuthenticationConfiguration中定义了这个属性,意思要收集所有GlobalAuthenticationConfigurerAdapter类型的类。GlobalAuthenticationConfigurerAdapter实现了SecurityConfigurer接口,是用来给全局的AuthenticationManagerBuilder提供配置的。
private List<GlobalAuthenticationConfigurerAdapter> globalAuthConfigurers = Collections.emptyList();
AuthenticationConfiguration中恰好提供了两个bean 一个InitializeUserDetailsBeanManagerConfigurer,另一个是InitializeAuthenticationProviderBeanManagerConfigurer。从名称可以看出来InitializeUserDetailsBeanManagerConfigurer是用来配置UserDetails的
@Bean public static InitializeUserDetailsBeanManagerConfigurer initializeUserDetailsBeanManagerConfigurer( ApplicationContext context) { return new InitializeUserDetailsBeanManagerConfigurer(context); } @Bean public static InitializeAuthenticationProviderBeanManagerConfigurer initializeAuthenticationProviderBeanManagerConfigurer( ApplicationContext context) { return new InitializeAuthenticationProviderBeanManagerConfigurer(context); }
收集了配置类之后 当authBuilder.build() 触发,创建管理器的时候,会触发流程中的configure 完成配置
private void configure() throws Exception { Collection<SecurityConfigurer<O, B>> configurers = getConfigurers(); for (SecurityConfigurer<O, B> configurer : configurers) { configurer.configure((B) this); } }
InitializeUserDetailsBeanManagerConfigurer
springsecurity采取了如下的方式来配置
外层的配置器InitializeUserDetailsBeanManagerConfigurer 把内层包装的配置器InitializeUserDetailsManagerConfigurer 通过apply方式加入到一个配置器列表中 后续在构建 AuthenticationManager
时,会按顺序调用它的 configure(...)
方法。
InitializeUserDetailsManagerConfigurer的
接下来我们就研究下InitializeUserDetailsManagerConfigurer的configure方法,看他到底做了什么?
class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter { private final Log logger = LogFactory.getLog(getClass()); @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { List<BeanWithName<UserDetailsService>> userDetailsServices = getBeansWithName(UserDetailsService.class); if (auth.isConfigured()) { if (!userDetailsServices.isEmpty()) { this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. " + "UserDetailsService beans will not be used for username/password login. " + "Consider removing the AuthenticationProvider bean. " + "Alternatively, consider using the UserDetailsService in a manually instantiated " + "DaoAuthenticationProvider."); } return; } if (userDetailsServices.isEmpty()) { return; } else if (userDetailsServices.size() > 1) { List<String> beanNames = userDetailsServices.stream().map(BeanWithName::getName).toList(); this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. " + "Global Authentication Manager will not use a UserDetailsService for username/password login. " + "Consider publishing a single UserDetailsService bean.", userDetailsServices.size(), beanNames)); return; } var userDetailsService = userDetailsServices.get(0).getBean(); var userDetailsServiceBeanName = userDetailsServices.get(0).getName(); PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class); CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.class); DaoAuthenticationProvider provider; if (passwordEncoder != null) { provider = new DaoAuthenticationProvider(passwordEncoder); } else { provider = new DaoAuthenticationProvider(); } provider.setUserDetailsService(userDetailsService); if (passwordManager != null) { provider.setUserDetailsPasswordService(passwordManager); } if (passwordChecker != null) { provider.setCompromisedPasswordChecker(passwordChecker); } provider.afterPropertiesSet(); auth.authenticationProvider(provider); this.logger.info(LogMessage.format( "Global AuthenticationManager configured with UserDetailsService bean with name %s", userDetailsServiceBeanName)); }
首先是一段防御性检查
List<BeanWithName<UserDetailsService>> userDetailsServices = getBeansWithName(UserDetailsService.class); if (auth.isConfigured()) { if (!userDetailsServices.isEmpty()) { this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. " + "UserDetailsService beans will not be used for username/password login. " + "Consider removing the AuthenticationProvider bean. " + "Alternatively, consider using the UserDetailsService in a manually instantiated " + "DaoAuthenticationProvider."); } return; }
🔍 1. auth.isConfigured()
是什么意思?
AuthenticationManagerBuilder
(简称auth
)在构建过程中,可以被多次配置。- 一旦某个
AuthenticationProvider
被显式添加(比如通过auth.authenticationProvider(...)
),Spring Security 就会标记它为 “已配置” (configured)。 isConfigured()
返回true
表示:已经有开发者或别的配置类手动添加了认证提供者。
👉 换句话说:你已经自己动手配置了认证逻辑,Spring Security 就不该再“自作聪明”地帮你自动配置了。
🔍 2. 为什么“已配置”还要发警告?
因为此时:
- 系统中存在
UserDetailsService
Bean(比如你写了@Bean UserDetailsService myUserDetailsService()
) - 但
AuthenticationManager
已经被别的AuthenticationProvider
配置过了 - 所以 Spring Security 不会自动使用你的
UserDetailsService
⚠️ 这就是问题所在:
你定义了一个
UserDetailsService
,本意是希望它用于用户名/密码登录,
但由于你提前配置了别的AuthenticationProvider
(比如 JWT、OAuth2、Ldap 等),
导致这个UserDetailsService
被完全忽略了!
🚨 警告在说什么?(翻译成人话)
“检测到你已经手动配置了
AuthenticationProvider
,
所以 Spring Security 不会自动使用你定义的UserDetailsService
来处理用户名/密码登录。但你现在又定义了
UserDetailsService
,这很可能是你期望它被用于登录。可惜它根本没被用上!这可能是配置错误。
建议:
- 要么删掉那个手动配置的
AuthenticationProvider
,让 Spring 自动用UserDetailsService
- 要么把你这个
UserDetailsService
主动加到你手动创建的DaoAuthenticationProvider
里去,别让它被忽略。”
if (userDetailsServices.isEmpty()) { return; } else if (userDetailsServices.size() > 1) { List<String> beanNames = userDetailsServices.stream().map(BeanWithName::getName).toList(); this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. " + "Global Authentication Manager will not use a UserDetailsService for username/password login. " + "Consider publishing a single UserDetailsService bean.", userDetailsServices.size(), beanNames)); return; }
⚠️ 注意:一个 DaoAuthenticationProvider
只能绑定一个 UserDetailsService
。这里再一次确保容器中只有一个UserDetailsService,框架只有发现容器中只有一个UserDetailsService的时候,才会帮助我们自动创建一个DaoAuthenticationProvider
DaoAuthenticationProvider provider; if (passwordEncoder != null) { provider = new DaoAuthenticationProvider(passwordEncoder); } else { provider = new DaoAuthenticationProvider(); } provider.setUserDetailsService(userDetailsService); if (passwordManager != null) { provider.setUserDetailsPasswordService(passwordManager); } if (passwordChecker != null) { provider.setCompromisedPasswordChecker(passwordChecker); } provider.afterPropertiesSet(); auth.authenticationProvider(provider);
这段就是创建一个DaoAuthenticationProvider,然后调用auth.authenticationProvider(provider); 设置进去。
总结起来:
userDetailsService是谁放到容器中的呢?
UserDetailsServiceAutoConfiguration 自动配置类在检查发现用户没有主动配置AuthenticationManager,AuthenticationProvider,UserDetailsService后,会创建一个基于内存的InMemoryUserDetailsManager
@AutoConfiguration @ConditionalOnClass(AuthenticationManager.class) @Conditional(MissingAlternativeOrUserPropertiesConfigured.class) @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder") public class UserDetailsServiceAutoConfiguration { private static final String NOOP_PASSWORD_PREFIX = "{noop}"; private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\{.+}.*$"); private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class); @Bean public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) { SecurityProperties.User user = properties.getUser(); List<String> roles = user.getRoles(); return new InMemoryUserDetailsManager(User.withUsername(user.getName()) .password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())) .roles(StringUtils.toStringArray(roles)) .build()); } private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) { String password = user.getPassword(); if (user.isPasswordGenerated()) { logger.warn(String.format( "%n%nUsing generated security password: %s%n%nThis generated password is for development use only. " + "Your security configuration must be updated before running your application in " + "production.%n", user.getPassword())); } if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) { return password; } return NOOP_PASSWORD_PREFIX + password; }
所以我们想要覆盖这种行为,可以自己配置一个userDetailsService,比如创建一个基于数据库的。这样这个内存的就会失效,采用用户自定义的
结论
基于以上的分析,只要我们配置了userDetailsService,框架会自动采用这个配置。将其应用在认证管理器中。
从图中调试我们也看到 最后这个userDetailsService作用在全局认证管理器parent中了。
更多推荐
所有评论(0)