今天有个同窗告诉我,在Security Learning项目的day11分支中出现了一个问题,验证码登陆和其它登陆不兼容了,出现了No Provider异常。还有这事?我赶忙跑了一遍还真是,看来我大意了,不过最终找到了缘由,问题就出在AuthenticationManager
的初始化上。自定义了一个UseDetailService
和AuthenticationProvider
以后AuthenticationManager
的默认初始化出问题了。html
虽然在Spring Security 实战干货:图解认证管理器AuthenticationManager一文中对AuthenticationManager
的流程进行了分析,可是仍是不够深刻,以致于出现了问题。今天就把这个坑补了。java
关于AuthenticationManager
的初始化,流程部分请看这一篇文章,里面有流程图。在流程图中咱们提到了AuthenticationManager
的默认初始化是由AuthenticationConfiguration
完成的,可是只是一笔带过,具体的细节没有搞清楚。如今就搞定它。app
AuthenticationConfiguration
初始化AuthenticationManager
的核心方法就是下面这个方法:ide
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
是如何注入Spring IoC的?ui
AuthenticationManagerBuilder
注入的过程也是在AuthenticationConfiguration
中完成的,注入的是其内部的一个静态类DefaultPasswordEncoderAuthenticationManagerBuilder
,这个类和Spring Security的主配置类WebSecurityConfigurerAdapter
的一个内部类同名,这两个类几乎逻辑相同,没有什么特别的。具体使用哪一个由WebSecurityConfigurerAdapter.disableLocalConfigureAuthenticationBldr
决定。this
其参数
ObjectPostProcessor<T>
抽空会讲它的做用。代理
另外一个问题是
GlobalAuthenticationConfigurerAdapter
从哪儿来?code
AuthenticationConfiguration
包含下面自动注入GlobalAuthenticationConfigurerAdapter
的方法:htm
@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
的注入。
到此为何在认证的时候找不到缘由终于找到了,原来我在使用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
。
这一篇对于须要多种认证方式并存的Spring Security配置很是重要,若是你在配置中不注意,很容易引起No Provider ……
的异常。因此有颇有必要学习一下。
关注公众号:Felordcn 获取更多资讯