今天搞懂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上
    1. 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中了。 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐