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中了。

相关推荐
曹牧25 分钟前
文档格式:OFD
java
豆角焖肉1 小时前
Maven进阶与搭建私服
java·maven
大不点wow1 小时前
Java序列化与反序列化:让对象走出JVM
java·开发语言·jvm
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
噢,我明白了1 小时前
Java中日期和字符串的处理
java·开发语言·日期
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
dkbnull1 小时前
Spring Boot请求处理组件对比详解
java·spring boot
jun_bai1 小时前
Orthanc服务器使用java上传dicom影像文件
java·运维·服务器