1. 前言
明天有个同学通知我,在 Security Learning 我的项目的 day11 分支中呈现了一个问题,验证码登录和其它登录不兼容了,呈现了 No Provider 异样。还有这事?我连忙跑了一遍还真是,看来我粗心了,不过最终找到了起因,问题就出在 AuthenticationManager
的初始化上。自定义了一个 UseDetailService
和AuthenticationProvider
之后 AuthenticationManager
的默认初始化出问题了。
尽管在 Spring Security 实战干货:图解认证管理器 AuthenticationManager 一文中对 AuthenticationManager
的流程进行了剖析,然而还是不够深刻,以至于呈现了问题。明天就把这个坑补了。
2. AuthenticationManager 的初始化
对于 AuthenticationManager
的初始化,流程局部请看这一篇文章,外面有流程图。在流程图中咱们提到了 AuthenticationManager
的默认初始化是由 AuthenticationConfiguration
实现的,然而只是一笔带过,具体的细节没有搞清楚。当初就搞定它。
AuthenticationConfiguration
AuthenticationConfiguration
初始化 AuthenticationManager
的外围办法就是上面这个办法:
public AuthenticationManager getAuthenticationManager() throws Exception {
// 先判断 AuthenticationManager 是否初始化
if (this.authenticationManagerInitialized) {
// 如果曾经初始化 那么间接返回初始化的
return this.authenticationManager;
}
// 否则就去 Spring IoC 中获取其构建类
AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class);
// 如果不是第一次构建 如同是每次总要通过 Builder 来进行构建
if (this.buildingAuthenticationManager.getAndSet(true)) {
// 返回 一个委托的 AuthenticationManager
return new AuthenticationManagerDelegator(authBuilder);
}
// 如果是第一次通过 Builder 构建 将全局的认证配置整合到 Builder 中 那么当前就不必再整合全局的配置了
for (GlobalAuthenticationConfigurerAdapter config : globalAuthConfigurers) {authBuilder.apply(config);
}
// 构建 AuthenticationManager
authenticationManager = authBuilder.build();
// 如果构建后果为 null
if (authenticationManager == null) {
// 再次尝试去 Spring IoC 获取懒加载的 AuthenticationManager Bean
authenticationManager = getAuthenticationManagerBean();}
// 批改初始化状态
this.authenticationManagerInitialized = true;
return authenticationManager;
}
依据下面的正文,AuthenticationManager
的初始化流程是分明的。然而又引出来了两个问题,我将另起两个章节来剖析这两个问题。
AuthenticationManagerBuilder
第一个问题是
AuthenticationManagerBuilder
是如何注入 Spring IoC 的?
AuthenticationManagerBuilder
注入的过程也是在 AuthenticationConfiguration
中实现的,注入的是其外部的一个动态类 DefaultPasswordEncoderAuthenticationManagerBuilder
,这个类和 Spring Security 的主配置类WebSecurityConfigurerAdapter
的一个外部类同名,这两个类简直逻辑雷同,没有什么特地的。具体应用哪个由 WebSecurityConfigurerAdapter.disableLocalConfigureAuthenticationBldr
决定。
其参数
ObjectPostProcessor<T>
抽空会讲它的作用。
GlobalAuthenticationConfigurerAdapter
另一个问题是
GlobalAuthenticationConfigurerAdapter
从哪儿来?
AuthenticationConfiguration
蕴含上面主动注入 GlobalAuthenticationConfigurerAdapter
的办法:
@Autowired(required = false)
public void setGlobalAuthenticationConfigurers(List<GlobalAuthenticationConfigurerAdapter> configurers) {configurers.sort(AnnotationAwareOrderComparator.INSTANCE);
this.globalAuthConfigurers = configurers;
}
该办法会依据它们各自的 Order
进行排序。该排序的意义在于 AuthenticationManagerBuilder
在执行构建 AuthenticationManager
时会依照排序的先后执行 GlobalAuthenticationConfigurerAdapter
的configure
办法。
全局认证配置
第一个为EnableGlobalAuthenticationAutowiredConfigurer
, 它目前除了打印一下初始化信息没有什么理论作用。
认证处理器初始化注入
第二个为InitializeAuthenticationProviderBeanManagerConfigurer
,外围办法为其内部类的实现:
@Override
public void configure(AuthenticationManagerBuilder auth) {
//
// 如果存在 AuthenticationProvider 曾经注入 或者 曾经有 AuthenticationManager 被代理
if (auth.isConfigured()) {return;}
// 尝试从 Spring IoC 获取 AuthenticationProvider
AuthenticationProvider authenticationProvider = getBeanOrNull(AuthenticationProvider.class);
// 获取不到就中断
if (authenticationProvider == null) {return;}
// 获取失去就配置到 AuthenticationManagerBuilder 中,最终会配置到 AuthenticationManager 中
auth.authenticationProvider(authenticationProvider);
}
这里的 getBeanOrNull
办法如果不认真看的话是有误区的,外围代码如下:
String[] userDetailsBeanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
.getBeanNamesForType(type);
// Spring IoC 不能同时存在多个 type 相干类型的 Bean 否则无奈注入
if (userDetailsBeanNames.length != 1) {return null;}
如果 Spring IoC 容器中存在了多个 AuthenticationProvider
,那么这些AuthenticationProvider
就不会失效。
用户详情管理器初始化注入
第三个为InitializeUserDetailsBeanManagerConfigurer
,优先级低于下面。它的外围办法为:
public void configure(AuthenticationManagerBuilder auth) throws Exception {if (auth.isConfigured()) {return;}
// 不能有多个 否则 就中断
UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class);
if (userDetailsService == null) {return;}
// 开始配置一般 明码认证器 DaoAuthenticationProvider
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
if (passwordEncoder != null) {provider.setPasswordEncoder(passwordEncoder);
}
if (passwordManager != null) {provider.setUserDetailsPasswordService(passwordManager);
}
provider.afterPropertiesSet();
auth.authenticationProvider(provider);
}
跟 InitializeAuthenticationProviderBeanManagerConfigurer
流程差不多,只不过这里次要解决的是 UserDetailsService
、DaoAuthenticationProvider
。当执行到下面这个办法时,如果 Spring IoC 容器中存在了多个UserDetailsService
,那么这些UserDetailsService
就不会失效,影响 DaoAuthenticationProvider
的注入。
3. 水落石出
到此为什么在认证的时候找不到起因终于找到了,原来我在应用 Spring Security 默认配置时 (留神这个前提),向Spring IoC 注入了多个 UserDetailsService
导致 DaoAuthenticationProvider
没有失效。也就是说在一套配置中如果你存在多个 UserDetailsService
的 Spring Bean 将会影响 DaoAuthenticationProvider
的注入。
然而我依然须要注入多个
AuthenticationProvider
怎么办?
首先把你须要配置的 AuthenticationProvider
注入 Spring IoC,而后在HttpSecurity
中这么写:
protected void configure(HttpSecurity http) throws Exception {ApplicationContext context = http.getSharedObject(ApplicationContext.class);
CaptchaAuthenticationProvider captchaAuthenticationProvider = context.getBean("captchaAuthenticationProvider", CaptchaAuthenticationProvider.class);
http.authenticationProvider(captchaAuthenticationProvider);
// 省略
}
有几个 AuthenticationProvider
你就依照下面配置几个。
个别状况下一个
UserDetailsService
对应一个AuthenticationProvider
。
4. 总结
这一篇对于须要多种认证形式并存的 Spring Security 配置十分重要,如果你在配置中不留神,很容易引发 No Provider ……
的异样。所以有很有必要学习一下。
关注公众号:Felordcn 获取更多资讯
集体博客:https://felord.cn