【Java】Spring Security 实战干货:AuthenticationManager的初始化细节

Spring Security 实战干货:AuthenticationManager的初始化细节

码农小胖哥发布于 今天 06:37

【Java】Spring Security 实战干货:AuthenticationManager的初始化细节

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注入的过程也是在AuthenticationConfiguration中完成的,注入的是其内部的一个静态类DefaultPasswordEncoderAuthenticationManagerBuilder,这个类和Spring Security的主配置类WebSecurityConfigurerAdapter的一个内部类同名,这两个类几乎逻辑相同,没有什么特别的。具体使用哪个由WebSecurityConfigurerAdapter.disableLocalConfigureAuthenticationBldr决定。

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,核心方法为其内部类的实现:

@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流程差不多,只不过这里主要处理的是UserDetailsServiceDaoAuthenticationProvider。当执行到上面这个方法时,如果 Spring IoC 容器中存在了多个UserDetailsService,那么这些UserDetailsService就不会生效,影响DaoAuthenticationProvider的注入。

3. 真相大白

到此为什么在认证的时候找不到原因终于找到了,原来我在使用Spring Security默认配置时(注意这个前提),向Spring IoC注入了多个UserDetailsService导致DaoAuthenticationProvider没有生效。也就是说在一套配置中如果你存在多个UserDetailsService的Spring Bean将会影响DaoAuthenticationProvider的注入。

首先把你需要配置的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你就按照上面配置几个。

4. 总结

这一篇对于需要多种认证方式并存的Spring Security配置非常重要,如果你在配置中不注意,很容易引发No Provider ……的异常。所以有很有必要学习一下。

关注公众号:Felordcn 获取更多资讯

个人博客:https://felord.cn

java

阅读 82发布于 今天 06:37

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议

avatar

码农小胖哥

技术公众号:<span>Felordcn</span> 欢迎关注

3.2k 声望

4.2k 粉丝

0 条评论

得票时间

avatar

码农小胖哥

技术公众号:<span>Felordcn</span> 欢迎关注

3.2k 声望

4.2k 粉丝

宣传栏

【Java】Spring Security 实战干货:AuthenticationManager的初始化细节

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注入的过程也是在AuthenticationConfiguration中完成的,注入的是其内部的一个静态类DefaultPasswordEncoderAuthenticationManagerBuilder,这个类和Spring Security的主配置类WebSecurityConfigurerAdapter的一个内部类同名,这两个类几乎逻辑相同,没有什么特别的。具体使用哪个由WebSecurityConfigurerAdapter.disableLocalConfigureAuthenticationBldr决定。

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,核心方法为其内部类的实现:

@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流程差不多,只不过这里主要处理的是UserDetailsServiceDaoAuthenticationProvider。当执行到上面这个方法时,如果 Spring IoC 容器中存在了多个UserDetailsService,那么这些UserDetailsService就不会生效,影响DaoAuthenticationProvider的注入。

3. 真相大白

到此为什么在认证的时候找不到原因终于找到了,原来我在使用Spring Security默认配置时(注意这个前提),向Spring IoC注入了多个UserDetailsService导致DaoAuthenticationProvider没有生效。也就是说在一套配置中如果你存在多个UserDetailsService的Spring Bean将会影响DaoAuthenticationProvider的注入。

首先把你需要配置的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你就按照上面配置几个。

4. 总结

这一篇对于需要多种认证方式并存的Spring Security配置非常重要,如果你在配置中不注意,很容易引发No Provider ……的异常。所以有很有必要学习一下。

关注公众号:Felordcn 获取更多资讯

个人博客:https://felord.cn

以上是 【Java】Spring Security 实战干货:AuthenticationManager的初始化细节 的全部内容, 来源链接: utcz.com/a/107881.html

回到顶部