前言
本文是对 Spring Security Core 4.0.4 Release 进行源码分析的系列文章之一;
本系列开始,将讲解有关 Spring Security 的配置相关的内容;
本文为作者的原创作品,转载需注明出处;
简介
该篇文章是继上一篇博文 Spring Security 源码分析九:Java config - HttpSecurity & SecurityConfigurer 的延续,上篇博文中,笔者止步于 UserDetailsAwareConfigurer 小节,简单介绍了一下 UserDetailsAwareConfigurer 该配置类实例是 AuthenticationManagerBuilder 实例的 configures,辅助其进行构建操作;
其构建过程等价于 RestSecurityConfig、WebSecurityConfig 之于 WebSecurity;FormLoginConfigurer、HttpBasicConfigurer 之于 HttpSecurity;
回顾
先来回顾一下什么是 InMemoryUserDetailsManager 和 JdbcUserDetailsManager,回顾一下 Spring Security 源码分析三:Core Services 核心服务 中的类图
从类图中可以清晰的看到,JdbcUserDetailsManager、InMemoryUserDetailsManager 均是 DaoAuthenticationProvider 的重要属性,提供了如何管理用户的方式,JdbcUserDetailsManager 提供了 Jdbc 的方式来增
、删
、查
、改
用户信息,而 InMemoryUserDetailsManager 则是提供了内存的方式;
而后面我们将看到 InMemoryUserDetailsManagerConfigurer 和 JdbcUserDetailsManagerConfigurer 均是封装了 InMemoryUserDetailsManager 和 JdbcUserDetailsManager 来分别以内存
的方式或者是以数据库
的方式来增
、删
、查
、改
用户信息;
类图
SecurityConfigurer
SecurityConfigurer Overall
下面这张类图正是前叙文章中所介绍到的有关 SecurityConfigurer 的全景图;
这张图中可以清晰的看到 AuthenticationManagerBuilder 与InMemoryUserDetailsManagerConfigurer
、JdbcUserDetailsManagerConfigurer
之间的关系,它们之间是包含与被包含的关系;并在 AuthenticationManagerBuilder 构建过程中使用到这些 configures;
UserDetailsAwareConfigurer
笔者就 UserDetailsAwareConfigurer 分支做进一步分析;该类图中比较核心的是 AbstractDaoAuthenticationConfigurer;通过其扩展出比较典型的实体类 InMemoryUserDetailsManagerConfigurer 和 DaoAuthenticationConfigurer;
AbstractDaoAuthenticationConfigurer
该方法最核心的是两属性和两方法,首先,来看看这两个非常重要的属性,
provider
1
private DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider -> DaoAuthenticationProvider
- userDetailsService
从回顾中可以看到,DaoAuthenticationProvider 与 UserDetailsService 是包含关系,既是 UserDetailsService 对象是 DaoAuthenticationProvider 的一个属性;UserDetailsService 由 UserDetailsManager 接口实现,其实现类可以是 InMemoryUserDetailsManagerConfigurer 或者是 JdbcUserDetailsManagerConfigurer;
然后,来看看这两个重要的方法
构造方法
1
2
3
4protected AbstractDaoAuthenticationConfigurer(U userDetailsService) {
this.userDetailsService = userDetailsService;
provider.setUserDetailsService(userDetailsService);
}上面的构造方法中的逻辑非常的重要;首先,通过构造函数参数 userDetailsService 将其传值给 this.userDetailsService,然后,将 userDetailsService 赋值给 provider;也就是将 InMemoryUserDetailsManager 或者是 JdbcUserDetailsManager 对象赋值给 provider;
configure(B builder)
1
2
3
4
5
public void configure(B builder) throws Exception {
provider = postProcess(provider);
builder.authenticationProvider(provider);
}这里的泛型 B 指的就是 AuthenticationManagerBuilder 对象,configure 的核心逻辑就是将 provider 赋值给 builder;
InMemoryUserDetailsManagerConfigurer
1 | public class InMemoryUserDetailsManagerConfigurer<B extends ProviderManagerBuilder<B>> |
InMemoryUserDetailsManagerConfigurer 的实现逻辑非常的简单,就是通过构造函数创建 InMemoryUserDetailsManager 对象,并通过 super 调用到 AbstractDaoAuthenticationConfigurer 的构造函数,正如在 AbstractDaoAuthenticationConfigurer 中分析的那样,该构造函数中将 InMemoryUserDetailsManager 作为 userDetailsService 参数赋值给了 provider 对象;
DaoAuthenticationConfigurer
1 | public class DaoAuthenticationConfigurer<B extends ProviderManagerBuilder<B>, U extends UserDetailsService> |
从该类的定义中可以知道,可以通过其构造函数传递用户自定义的 UserDetailsService 对象;这样就可以提供用户自定义的 UserDetailsManager 对象了,而不再使用默认的 InMemoryUserDetailsManager 或者 JdbcUserDetailsManager 了;
DaoAuthenticationConfigurer 可以通过 AuthenticationManagerBuilder 的方法 userDetailsService(T userDetailsService) 来进行扩展;
SecurityBuilder
AuthenticationManagerBuilder
由类图可知,AuthenticationManagerBuilder 是一个 SecuirtyBuilder 对象;笔者就核心的属性和方法进行分析如下;
首先,来看看相关的核心属性,
- AuthenticationManager parentAuthenticationManager
当通过 performBuild() 方法构建 ProviderManager 的时候,将会使用 parentAuthenticationManager 作为该 ProviderManager 对象的 parent authentication manager; - List
authenticationProviders
保存用于构造 AuthenticationManager 的 AuthenticationProvider 对象;从回顾章节可知,AuthenticationManager 与 AuthenticationProvider 是 1 对多的关系; - Boolean eraseCredentials
当用户验证结束以后是否需要在 Authentication 对象中将其密码给删掉;
其次,来看看相关的核心方法,
inMemoryAuthentication()
1
2
3
4public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
throws Exception {
return apply(new InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder>());
}提供给用户来定制所需的 UserDetailsManager 既是 UserDetailsService 对象;比如通过如下的方法来进行定制,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1) (
static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public void configUser(AuthenticationManagerBuilder builder) throws Exception {
builder
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER").and()
.withUser("manager").password("password").roles("MANAGER");
}
......
}jdbcAuthentication()
和 inMemoryAuthentication() 方法类似,提供这样的接口供用户来定制相应的 UserDetailsManager 既是 UserDetailsService 对象;userDetailsService(T userDetailsService)
1
2
3
4
5public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
T userDetailsService) throws Exception {
this.defaultUserDetailsService = userDetailsService;
return apply(new DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T>(userDetailsService));
}从该方法中可以直观的看到其目的是为 AuthenticationProvider 提供用户自定义的 UserDetailsService 对象,当内置的 InMemoryUserDetailsManager 或 JdbcUserDetailsManager UserDetailsService 不能满足用户需要的时候,可以使用该方法来进行扩展;该逻辑是通过 DaoAuthenticationConfigurer 实现的;
构建过程
概述
将这个构建过程的核心内容总结如下,
在整个构建过程中,AuthenticationManagerBuilder 和 AuthenticationManager 都包含两种类型的角色,全局的
和局部的
;全局的 AuthenticationManagerBuilder 生成全局的 AuthenticationManager,局部的 AuthenticationManagerBuilder 生成局部的 AuthenticationManager;与 HttpSecurity 相关的某一条安全验证链使用的正是由“局部的 AuthenticationManagerBuilder”所生成的“局部的 AuthenticationManager”来对用户的身份进行认证的,只是“全局的 AuthenticationManager”将作为“局部的 AuthenticationManager”的 parent authentication manager,当“局部的 AuthenticationManager”验证失败后,会使用 parent authentication manager;
另外,“局部的 AuthenticationManagerBuilder” 是 HttpSecurity 的一个私有属性,也就是某条安全链所专有的;也就是说,“用户可以为每条不同的安全链自定义不同的 AuthenticationManager 来对用户执行验证操作”;
全局 AuthenticationManagerBuilder 的初始化和定制流程
初始化过程
该初始化流程是通过 @EnableWebSecurity 为入口调用 @EnableGlobalAuthentication 注解加载 AuthenticationConfiguration 来加载该 AuthenticationManagerBuilder 对象到 Spring 容器中的;用下面的这张类图来总结这个过程,
从类图的逻辑中我们可以清晰的看到,首先通过 @EnableWebSecurity 注解作为入口,调用 @EnableGlobalAuthentication 注解,该注解将会加载 AuthenticationConfiguration 配置类( 通过 @Configuration 注解的类 ),然后通过 @Bean 方法 getAuthentiationManager() 初始化得到 AuthenticationManagerBuilder,并将其注入到 Spring 容器中,特别特别需要注意
的是,这里初始化所得到的 AuthenticationManagerBuilder 就是全局的 AuthenticationManagerBuilder
对象;下面,笔者就一些核心部分源码摘录如下,
1 | (value = java.lang.annotation.RetentionPolicy.RUNTIME) |
可以看到,在加载 @EnableWebSecurity 注解的同时会加载 @EnableGlobalAuthentication 注解,
___@EnableGlobalAuthentication.java___
1 | (value = java.lang.annotation.RetentionPolicy.RUNTIME) |
可见当在加载 @EnableGlobalAuthentication 注解的时候会通过 @Import 注解加载配置类 AuthenticationConfiguration
AuthenticationConfiguration.java
通过 @Bean 方法 authenticationManagerBuilder(objectPostProcessor) 初始化得到一个 AuthenticationManagerBuilder 实例并以 Spring bean 的形式返回,载入 Spring 容器中,作为全局的 AuthenticationManagerBuilder
对象;
1 |
|
用户自定义方式
那么这里,我们将如何对全局的 AuthenticationManagerBuilder
对象进行定制?比如设置 UserDetailsManager 既 UserDetailsService 等;很简单,在初始化的过程中直接从 Spring 容器中获取得到 AuthenticationManagerBuilder 对象,然后对其进行自定义即可;好比下面这个例子,为全局的 AuthenticationManagerBuilder
对象设置 InMemoryUserDetailsManager 对象,然后在全局的 AuthenticationManagerBuilder
对象执行构建的过程中,将返回一个包含了 InMemoryUserDetailsManager : UserDetailsService 的 AuthenticationProvider 的 AuthenticationManager 对象;
1 |
|
更多相关的用户定制化方法参考 AuthenticationManagerBuilder 部分的相关介绍;
使用 Sequence Diagram 进行归纳总结
上述的相关步骤可以通过下面的 Sequence Diagram 来进行归纳和总结;
构建流程分析
笔者画了如下的一张完整的 sequence diagram 诠释了全局的 AuthenticationManagerBuilder
和局部的 AuthenticationManagerBuilder
的构建过程;
该流程图分为四块,分别是左上角、右上角、中间和底部;每个部分对应不同的执行逻辑;笔者就这四个部分僬侥的进行描述如下,
左上角
此部分既是 Step 1 -> Step 1.1 结束;该部分逻辑既是局部的 AuthenticationManagerBuilder
的初始化流程;该逻辑是通过 WebSecurityConfigurerAdapter 的 @Autowired 方法 setObjectPostProcessor() 实现的;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
this.objectPostProcessor = objectPostProcessor;
authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
localConfigureAuthenticationBldr = new AuthenticationManagerBuilder(
objectPostProcessor) {
public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) {
authenticationBuilder.eraseCredentials(eraseCredentials);
return super.eraseCredentials(eraseCredentials);
}
};
}可见,该方法中通过初始化构建了一个 AuthenticationManagerBuilder 对象并将其赋值给 WebSecurityConfigurerAdapter 的局部变量 authenticationBuilder,也正因为如此,笔者将该对象称作
局部的 AuthenticationManagerBuilder
;后面我们可以看到,它将通过传参的方式,将该局部的 AuthenticationManagerBuilder
传递给与当前安全作用链相关的 HttpSecurity 对象中;右上角
从 Step 2 -> Step 3.2.1.1 结束;该部分主要是相关 SecurityConfigurer 的构建行为,主要描述了 DaoAuthenticationProvider 的生成过程,以及如何将 UserDetailsService 关联到 DaoAuthenticationProvider 对象中的;注意,这里是通过子类 InMemoryUserDetailsManagerConfigurer 的构造函数创建的 UserDetailsService 对象并通过 super 调用父类的构造函数,最终将 UserDetailsService 赋值给 DaoAuthenticationProvider 对象饿;中间部分
该部分涵盖了 Step 4 以及相关的所有的子步骤;该部分主要是描述全局的 AuthenticationManagerBuilder
的构造过程;该步骤是对 WebSecurity 构造步骤 1.1.1.1.4.3.1 的延续,既是从 WebSecurity 的构建过程中调用 HttpSecurity 执行构建的步骤;这里笔者通过上述的时序图对全局的 AuthenticationManagerBuilder
的构造过程进行详细的诠释;下面笔者就几个核心的步骤进行简要的描述,4.1.1
该步骤试图从 Spring 容器中获得得到该
全局的 AuthenticationManagerBuilder
对象;4.1.1.1.1 build process
从该步骤正式开始对
全局的 AuthenticationManagerBuilder
对象的构建过程;在执行到 configure 的步骤的时候,将会一次调用所有的 configurers,比如 InMemoryUserDetailsManagerConfigurer、JdbcUserDetailsManagerConfigurer 等,的 configure 方法来执行相关的配置操作,步骤 4.1.1.1.1.1.2.1.1 configure(B builder) 中将会把 DaoAuthenticationProvider 对象作为参数注入到 builder 中;最后,该步骤执行完以后,将会生成一个
全局的 Authentication Manager
并返回;4.1.1.1.1.1.3 perform build process
该步骤是整个 build 的最后一步,该步骤将会通过 ProviderManager 构造函数构建一个 ProviderManager 对象并返回;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected ProviderManager performBuild() throws Exception {
if (!isConfigured()) {
logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
return null;
}
ProviderManager providerManager = new ProviderManager(authenticationProviders,
parentAuthenticationManager);
if (eraseCredentials != null) {
providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
}
if (eventPublisher != null) {
providerManager.setAuthenticationEventPublisher(eventPublisher);
}
providerManager = postProcess(providerManager);
return providerManager;
}通过 DemoApplication 进行构建的过程,注意这里的参数,authenticationProviders 仅包含一个 DaoAuthenticationProvider 对象;parentAuthenticationManager 为
null
;4.1.2
正如时序图所注解的那样,这一步非常的关键,它将
全局的 Authentication Manager
作为参数传递给了局部的 Authenticaiton Manager Builder
对象;目的既是在局部的 Authenticaiton Manager Builder
对象的构建的过程中,将全局的 Authentication Manager
作为 parent auth manager 赋值给局部的 Authenticaiton Manager Builder
对象;4.1.3.1
这一步要注意的是,在构建 HttpSecurity 对象的时候,在其构造方法中,通过 setSharedObject(AuthenticationManagerBuilder.class, localAuthBuilder) 方法,将
局部的 Authenticaiton Manager Builder
对象设置为了当前的 HttpSecurity 的局部共享对象;后续我们将看到,HttpSecurity 正式根据此局部的 Authenticaiton Manager Builder
对象来执行构建的;底部
该部分包含 Step 6 以及相关所有的子步骤;该步骤主要描述了 HttpSecurity 如何通过其局部的 Authenticaiton Manager Builder
对象来执行构建并生成相应的 AuthenticationManager 的;笔者就相关核心步骤进行简要描述如下,6.1.2.1
从与当前 HttpSecurity 相关的共享对象池中获取得到
局部的 Authenticaiton Manager Builder
对象;准备后续的构建动作;6.1.2.2
使用
局部的 Authenticaiton Manager Builder
对象正式开始执行构建动作;6.1.2.2.3.1
该步骤是 perform build process 的核心步骤,生成 ProviderManager 并返回,这里返回的既是
局部的 Authentication Manager
;以 DemoApplication 中的配置为例,注意这里的参数,authenticationProviders 只包含了一个 AnonymousAuthenticationProvider 对象,看来是一个默认的 AuthenticationProvider 对象;parentAuthenticationManager 既是全局的 Authentication Manager
,这里为什么能过获取到全局的 Authentication Manager
参考步骤 4.1.2;6.1.2.3
将
局部的 Authentication Manager
作为 AuthenticationManager.class 类型设置到 HttpSecurity 私有的共享对象池中;这一步的意义非常之大,也就是说,在某个安全链的验证过程中,使用的是其局部的 Authentication Manager
;
最后用简短的几句话来总结一下上述复杂流程的目的,这里 Spring Security 设计者们想要达到的目的是,为每一个 Spring Security 安全链( 既 HttpSecurity )创建不同的局部的 Authentication Manager
,用于用户身份的安全验证;验证过程中,当局部的 Authentication Manager
验证失败以后将会使用其 parent Authentication Manager 既是全局的 Authentication Manager
来进行验证;
由此,我的脑海浮现出了这样一种场景,某一条安全链使用的是基于内存
的认证管理器对用户进行认证,而另外一条安全链使用的是基于数据库
的认证管理器对用户进行认证,再有一条安全链使用的是基于LDAP
的认证管理器对用户进行认证;