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