Spring Security 源码分析九:Java config - AuthenticationManagerBuilder

前言

本文是对 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;

回顾

先来回顾一下什么是 InMemoryUserDetailsManagerJdbcUserDetailsManager,回顾一下 Spring Security 源码分析三:Core Services 核心服务 中的类图

从类图中可以清晰的看到,JdbcUserDetailsManager、InMemoryUserDetailsManager 均是 DaoAuthenticationProvider 的重要属性,提供了如何管理用户的方式,JdbcUserDetailsManager 提供了 Jdbc 的方式来用户信息,而 InMemoryUserDetailsManager 则是提供了内存的方式;

而后面我们将看到 InMemoryUserDetailsManagerConfigurerJdbcUserDetailsManagerConfigurer 均是封装了 InMemoryUserDetailsManagerJdbcUserDetailsManager 来分别以内存的方式或者是以数据库的方式来用户信息;

类图

SecurityConfigurer

SecurityConfigurer Overall

下面这张类图正是前叙文章中所介绍到的有关 SecurityConfigurer 的全景图;

这张图中可以清晰的看到 AuthenticationManagerBuilder 与InMemoryUserDetailsManagerConfigurerJdbcUserDetailsManagerConfigurer之间的关系,它们之间是包含与被包含的关系;并在 AuthenticationManagerBuilder 构建过程中使用到这些 configures;

UserDetailsAwareConfigurer

spring secuirty UserDetailsManagerConfigurer 类图

笔者就 UserDetailsAwareConfigurer 分支做进一步分析;该类图中比较核心的是 AbstractDaoAuthenticationConfigurer;通过其扩展出比较典型的实体类 InMemoryUserDetailsManagerConfigurerDaoAuthenticationConfigurer

AbstractDaoAuthenticationConfigurer

该方法最核心的是两属性和两方法,首先,来看看这两个非常重要的属性,

  • provider

    1
    private DaoAuthenticationProvider provider = new DaoAuthenticationProvider();

    provider -> DaoAuthenticationProvider

  • userDetailsService
    回顾中可以看到,DaoAuthenticationProvider 与 UserDetailsService 是包含关系,既是 UserDetailsService 对象是 DaoAuthenticationProvider 的一个属性;UserDetailsService 由 UserDetailsManager 接口实现,其实现类可以是 InMemoryUserDetailsManagerConfigurer 或者是 JdbcUserDetailsManagerConfigurer

然后,来看看这两个重要的方法

  • 构造方法

    1
    2
    3
    4
    protected 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
    @Override
    public void configure(B builder) throws Exception {
    provider = postProcess(provider);
    builder.authenticationProvider(provider);
    }

    这里的泛型 B 指的就是 AuthenticationManagerBuilder 对象,configure 的核心逻辑就是将 provider 赋值给 builder;

InMemoryUserDetailsManagerConfigurer

1
2
3
4
5
6
7
8
9
10
public class InMemoryUserDetailsManagerConfigurer<B extends ProviderManagerBuilder<B>>
extends UserDetailsManagerConfigurer<B, InMemoryUserDetailsManagerConfigurer<B>> {

/**
* Creates a new instance
*/
public InMemoryUserDetailsManagerConfigurer() {
super(new InMemoryUserDetailsManager(new ArrayList<UserDetails>()));
}
}

InMemoryUserDetailsManagerConfigurer 的实现逻辑非常的简单,就是通过构造函数创建 InMemoryUserDetailsManager 对象,并通过 super 调用到 AbstractDaoAuthenticationConfigurer 的构造函数,正如在 AbstractDaoAuthenticationConfigurer 中分析的那样,该构造函数中将 InMemoryUserDetailsManager 作为 userDetailsService 参数赋值给了 provider 对象;

DaoAuthenticationConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
public class DaoAuthenticationConfigurer<B extends ProviderManagerBuilder<B>, U extends UserDetailsService>
extends
AbstractDaoAuthenticationConfigurer<B, DaoAuthenticationConfigurer<B, U>, U> {

/**
* Creates a new instance
* @param userDetailsService
*/
public DaoAuthenticationConfigurer(U userDetailsService) {
super(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
    4
    public 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
    @Configuration
    @EnableWebSecurity
    @Order(1)
    static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    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
    5
    public <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 容器中的;用下面的这张类图来总结这个过程,
spring secuirty @EnableGlobalAuthentication 类图

从类图的逻辑中我们可以清晰的看到,首先通过 @EnableWebSecurity 注解作为入口,调用 @EnableGlobalAuthentication 注解,该注解将会加载 AuthenticationConfiguration 配置类( 通过 @Configuration 注解的类 ),然后通过 @Bean 方法 getAuthentiationManager() 初始化得到 AuthenticationManagerBuilder,并将其注入到 Spring 容器中,特别特别需要注意的是,这里初始化所得到的 AuthenticationManagerBuilder 就是全局的 AuthenticationManagerBuilder对象;下面,笔者就一些核心部分源码摘录如下,

___@EnableWebSecurity.java___

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class, ObjectPostProcessorConfiguration.class,
SpringWebMvcImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {

/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}

可以看到,在加载 @EnableWebSecurity 注解的同时会加载 @EnableGlobalAuthentication 注解,

___@EnableGlobalAuthentication.java___

1
2
3
4
5
6
7
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}

可见当在加载 @EnableGlobalAuthentication 注解的时候会通过 @Import 注解加载配置类 AuthenticationConfiguration

AuthenticationConfiguration.java

通过 @Bean 方法 authenticationManagerBuilder(objectPostProcessor) 初始化得到一个 AuthenticationManagerBuilder 实例并以 Spring bean 的形式返回,载入 Spring 容器中,作为全局的 AuthenticationManagerBuilder对象;

1
2
3
4
5
@Bean
public AuthenticationManagerBuilder authenticationManagerBuilder(
ObjectPostProcessor<Object> objectPostProcessor) {
return new AuthenticationManagerBuilder(objectPostProcessor);
}

用户自定义方式

那么这里,我们将如何对全局的 AuthenticationManagerBuilder对象进行定制?比如设置 UserDetailsManager 既 UserDetailsService 等;很简单,在初始化的过程中直接从 Spring 容器中获取得到 AuthenticationManagerBuilder 对象,然后对其进行自定义即可;好比下面这个例子,为全局的 AuthenticationManagerBuilder对象设置 InMemoryUserDetailsManager 对象,然后在全局的 AuthenticationManagerBuilder对象执行构建的过程中,将返回一个包含了 InMemoryUserDetailsManager : UserDetailsService 的 AuthenticationProvider 的 AuthenticationManager 对象;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@EnableWebSecurity
@Order(1)
static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
public void configUser(AuthenticationManagerBuilder builder) throws Exception {
builder
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER").and()
.withUser("manager").password("password").roles("MANAGER");

}

......

}

更多相关的用户定制化方法参考 AuthenticationManagerBuilder 部分的相关介绍;

使用 Sequence Diagram 进行归纳总结

上述的相关步骤可以通过下面的 Sequence Diagram 来进行归纳和总结;
Spring Security AuthenticationConfiguration 时序图

构建流程分析

笔者画了如下的一张完整的 sequence diagram 诠释了全局的 AuthenticationManagerBuilder局部的 AuthenticationManagerBuilder的构建过程;

spring security authentication manager builder 构建过程时序图

该流程图分为四块,分别是左上角、右上角、中间和底部;每个部分对应不同的执行逻辑;笔者就这四个部分僬侥的进行描述如下,

  • 左上角
    此部分既是 Step 1 -> Step 1.1 结束;该部分逻辑既是局部的 AuthenticationManagerBuilder的初始化流程;该逻辑是通过 WebSecurityConfigurerAdapter 的 @Autowired 方法 setObjectPostProcessor() 实现的;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Autowired
    public void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
    this.objectPostProcessor = objectPostProcessor;

    authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
    localConfigureAuthenticationBldr = new AuthenticationManagerBuilder(
    objectPostProcessor) {
    @Override
    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
    @Override
    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的认证管理器对用户进行认证;