您的位置:首页 > 编程语言 > Java开发

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 ……
的异常。所以有很有必要学习一下。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: