UserDetailService是在什么环节生效的,为什么自定义之后就能被识别

今天搞懂3个问题:

1.为什么引入springsecurity后我们发现,框架会为我们生成一个基于内存的用户和密码?

2.如何实现基于数据库的用户的认证和授权?

3.为什么配置一个UserDetailService的Bean就能够起作用?

认证过程概述

认证请求被过滤器链拦截后,需要将认证请求委托给AuthenticationManager认证管理器 也就是说过滤器链中的过滤器是可以获取到对应的认证管理器的。这个是因为DefaultSecurityFilterChain过滤器链是由HttpSecurity 构建的。而HttpSecurity框架初始化的时候就创建了一个全局的认证管理器,代码如下:

HttpSecurityConfiguration负责配置HttpSecurity的Bean实例,将其配置了prototype类型。也就是每一个过滤器链都是一个独立全新的HttpSecurity。

scss 复制代码
@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中。

ini 复制代码
authenticationBuilder.parentAuthenticationManager(authenticationManager());
java 复制代码
//注入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是为了使全局的配置生效的。

less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(AuthenticationConfiguration.class)
public @interface EnableGlobalAuthentication {

}

AuthenticationConfiguration中配置了一个重要的bean

ini 复制代码
@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的方法

kotlin 复制代码
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提供配置的。

ini 复制代码
private List<GlobalAuthenticationConfigurerAdapter> globalAuthConfigurers = Collections.emptyList();

AuthenticationConfiguration中恰好提供了两个bean 一个InitializeUserDetailsBeanManagerConfigurer,另一个是InitializeAuthenticationProviderBeanManagerConfigurer。从名称可以看出来InitializeUserDetailsBeanManagerConfigurer是用来配置UserDetails的

typescript 复制代码
@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 完成配置

scss 复制代码
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方法,看他到底做了什么?

scala 复制代码
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));
   }

首先是一段防御性检查

kotlin 复制代码
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 里去,别让它被忽略。"
kotlin 复制代码
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

ini 复制代码
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

less 复制代码
@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中了。

相关推荐
萤丰信息3 分钟前
技术赋能安全:智慧工地构建城市建设新防线
java·大数据·开发语言·人工智能·智慧城市·智慧工地
用户4822137167755 分钟前
C++——字符串常量、二维数组、函数与指针的深度应用(补)
后端
用户4822137167756 分钟前
C++——类型转换
后端
lichenyang45315 分钟前
mongoose(对象文档模型库)的使用
后端
用户48221371677517 分钟前
C++——继承进阶
后端
带刺的坐椅22 分钟前
Java MCP 的鉴权?好简单的啦
java·鉴权·mcp·solon-ai
Pocker_Spades_A26 分钟前
飞算JavaAI家庭记账系统:从收支记录到财务分析的全流程管理方案
java·开发语言
33255_40857_2805930 分钟前
掌握分页艺术:MyBatis与MyBatis-Plus实战指南(10年Java亲授)
java·mybatis
洛卡卡了41 分钟前
数据库加密方案实践:我们选的不是最完美,但是真的够用了。
数据库·后端·面试
Java中文社群41 分钟前
淘宝首位程序员离职,竟投身AI新公司做这事!
人工智能·后端·程序员