Spring Security 实战干货:AuthenticationManager的初始化细节
2021-05-03 21:27
253 查看
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 ……的异常。所以有很有必要学习一下。
相关文章推荐
- Spring Security 实战干货:如何实现不同的接口不同的安全策略
- Spring Security 实战干货:如何实现不同的接口不同的安全策略
- 【WEB API项目实战干货系列】- WEB API入门(一)
- 关注C++细节——字面值初始化字符数组及字符串拷贝注意
- 【干货】个人工作文档节选:XAML MVVM 框架易用性细节优化Tips
- Android Studio 实战干货例程
- 大厂实战干货:项目经理面试就得这么盘
- 博主干货:DNS原理及实战配置指南
- 引用初始化的重要细节
- PHP高手干货分享:要大大提高PHP效率,不能不看的50个细节
- 【Spring实战】Spring容器初始化完成后执行初始化数据方法
- 关注C++细节——字面值初始化字符数组及字符串拷贝注意
- PHP高手干货分享:不能不看的50个细节!
- TCP 的三次握手,四次挥手和重要的细节—干货满满,建议细读
- spring实战三装配bean之Bean的作用域以及初始化和销毁Bean
- Spring security在MS-SQL下的初始化脚本
- Spring Security开发安全的REST服务 大神带你学习后端安全开发实战分享
- 干货 | Elasticsearch开发人员最佳实战指南
- 关于结构体初始化需要注意的一个细节
- Spring Security4.0.3源码分析之FilterChainProxy初始化