1. 前言

明天有个同学通知我,在Security Learning我的项目的day11分支中呈现了一个问题,验证码登录和其它登录不兼容了,呈现了No Provider异样。还有这事?我连忙跑了一遍还真是,看来我粗心了,不过最终找到了起因,问题就出在AuthenticationManager的初始化上。自定义了一个UseDetailServiceAuthenticationProvider之后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时会依照排序的先后执行GlobalAuthenticationConfigurerAdapterconfigure办法。

全局认证配置

第一个为EnableGlobalAuthenticationAutowiredConfigurer,它目前除了打印一下初始化信息没有什么理论作用。

认证处理器初始化注入

第二个为InitializeAuthenticationProviderBeanManagerConfigurer,外围办法为其内部类的实现:

@Overridepublic 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流程差不多,只不过这里次要解决的是UserDetailsServiceDaoAuthenticationProvider。当执行到下面这个办法时,如果 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