Spring Security 身份验证

本文将介绍 Spring Security 的身份验证功能的流程、配置及代码。

一、说明

在 Spring Security 中,身份验证一共分为两个部分:

  • 登录:给用户提供登录入口,使得用户可以在验证自己身份后登录进系统中
  • 登录状态保持:Spring Security 通过 Session 实现登录状态的保持

二、登录

1. 流程

  • 接收

    • 前端输入用户名及密码,传递给后端
    • 后端接收用户名和密码后,将其包装为一个 Authentication 的实例
  • 验证

    • 如果该请求是登录请求,则请求会被过滤器链中的 UsernamePasswordAuthenticationFilter 过滤,它将调用 ProviderManager 验证管理器进行验证

    • ProviderManager 验证管理器管理着多个 AuthenticationProvider 验证类组成的验证链,它将会依次调用验证类进行验证,一旦任何一个验证类验证成功,即代表验证成功

    • 验证成功后将会返回一个完整的 Authentication 实例,该实例会被放置到安全上下文 SecurityContext 中

2. Authentication

(1) Authentication

Authentication 在 Spring Security 中代表用户,包含用户密码、身份信息、角色信息、登录细节、是否通过校验的信息。

Authentication 是一个接口,它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public interface Authentication extends Principal, Serializable {

/**
* 获取角色的集合
*/
Collection<? extends GrantedAuthority> getAuthorities();

/**
* 获取验证凭据,即密码
*/
Object getCredentials();

/**
* 获取相关细节,例如 IP 地址、SessionId 等
*/
Object getDetails();

/**
* 获取被验证的主体,即被验证者的身份信息
*/
Object getPrincipal();

/**
* 指示当前用户是否已通过身份验证
*/
boolean isAuthenticated();

/**
* 用于设置当前用户的身份验证状态
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

}

(2) UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken 是 Authentication 的一个实现类,它旨在简单地呈现用户名和密码。

3. 验证管理器

在 Spring Security 中,ProviderManager 作为验证管理器。

它将会被过滤器链中的 UsernamePasswordAuthenticationFilter 调用,用于对用户的登录请求做校验。

它的作用是:

  • 遍历 AuthenticationProvider 集合
  • 判断 AuthenticationProvider 是否支持校验 Authentication 实例
  • 如果支持,则调用 AuthenticationProvider,将 Authentication 实例传入,由 AuthenticationProvider 进行校验
  • 一旦校验通过,则将 AuthenticationProvider 返回的完整的 Authentication 实例返回

代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ProviderManager {

/**
* 尝试对传递过来的 Authentication 实例进行身份验证
*/
public Authentication authenticate(Authentication authentication) {

for (AuthenticationProvider provider : getProviders()) {
// 判断provider是否支持校验此authentication
if (!provider.supports(authentication.getClass())) {
continue;
}
result = provider.authenticate(authentication);
if (result != null) {
// authentication对象中包含不完整用户信息和details(IP地址、SessionId等)
// result对象中包含完整用户信息
// 将details拷贝至result对象中,从而获得包含完整用户信息和details的对象
copyDetails(authentication, result);
break;
}
}
if (result != null) {
return result;
}
}

}

三、登录状态保持

如果开启了 Session,

  • 登录成功时,Spring Security 会将安全上下文存储至 Session 中
  • 接收到请求时,Spring Security 会尝试在 Session 中寻找安全上下文并使用

因此,用户的登陆状态得到了保持。

具体的 “登录状态保存” 和 “登录状态读取”,请看:

六、源码阅读 - 对 Session 的使用

四、配置 - 用户信息

1. UserDetails

在实际开发中,User 往往会以各种各样的方式存储在各种各样的地方,Spring Security 将不可避免地需要与各种各样的 User 实体类进行交互。

  • int ID, String name, String password - user 表 - 存储在 MySQL
  • int ID, String name - user表;int ID, String password - password 表 - 存储在 Mongo

为解决这一问题,Spring Security 定义了 UserDetails,要求实体类符合这一规范。

UserDetails 接口的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public interface UserDetails extends Serializable {

/**
* 获取由用户角色组成的集合
*/
Collection<? extends GrantedAuthority> getAuthorities();

/**
* 获取密码
*/
String getPassword();

/**
* 获取用户名
*/
String getUsername();

/**
* 获取用户是否过期
*/
boolean isAccountNonExpired();

/**
* 获取用户是否被锁定
*/
boolean isAccountNonLocked();

/**
* 获取用户的凭证是否过期
*/
boolean isCredentialsNonExpired();

/**
* 获取用户的启用状态
*/
boolean isEnabled();

}

2. UserDetailsService

UserDetailsService 的作用是:

被 AuthenticationProvider 验证类(通常是 DaoAuthenticationProvider 验证类)调用,返回用户的完整信息,以供 AuthenticationProvider 验证类进行比对。

3. 用户信息的存储

(1) 存储于内存中

可以在配置类中创建用户到内存,Spring Security 会在用户请求登录时根据请求中的部分信息(通常是用户名)查找完整的用户信息,进行比对。

1
2
3
4
auth
.inMemoryAuthentication()
.passwordEncoder(密码加密器)
.withUser(用户名).password(密码加密器.encode(密码)).roles(角色列表);

(2) 存储于其它地方

可以将用户存储于其它地方,此时便需要自定义 userDetails 查询器,以便在 Spring Security 需要时提供用户信息。

具体做法如下:

  • 将用户信息存储于某处

  • 自定义 userDetails 查询器,编写 UserDetailsService 实现类,在类中根据用户名读取用户信息并返回

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Service
    public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userDao.getByUsername(username);
    if (user == null) {
    throw new UsernameNotFoundException("数据库中无此用户!");
    }
    return user;
    }

    }
  • 配置自定义 userDetails 查询器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @EnableWebSecurity
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 自定义userDetails查询器
    auth.userDetailsService(myUserDetailsService).passwordEncoder(密码加密器);
    }
    }

五、配置 - 验证类

1. 自定义验证类

  • 创建类,实现 AuthenticationProvider 接口
  • 在类中使用 authentication.getName()authentication.getCredentials().toString() 方法获取用户名和密码
  • 自定义校验规则
  • 若校验通过,则返回包含角色信息的 Authentication 实例
  • 若校验失败,则返回 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class BackdoorAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication){
String name = authentication.getName();
String password = authentication.getCredentials().toString();

if ("root".equals(name)) {
// 放入权限
Collection<GrantedAuthority> authorityCollection = new ArrayList<>();
authorityCollection.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
authorityCollection.add(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken("root", password, authorityCollection);
} else {
return null;
}
}

@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

2. 配置 DaoAuthenticationProvider

DaoAuthenticationProvider 是 Spring Security 中的默认验证类,它将调用 UserDetailsService 根据登录请求中的部分信息查找完整信息,比对进行校验。

它有配置方法如下:

  • passwordEncoder():密码加密器(用于将明文密码加密)
  • userDetailsPasswordManager:密码修改器(用于对存储的用户信息中的密码进行修改)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
MyUserDetailsService myUserDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.userDetailsService(myUserDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}

···
}

六、源码阅读 - 对 Session 的使用

1. 默认策略

Session 默认会开启对 Session 的使用。

2. SecurityContextRepository

如果开启 Session,则 SessionManagementConfigurer 类的 init() 方法会将 SecurityContextRepository 设为 HttpSessionSecurityContextRepository 对象;如果不开启 Session,则该方法会将 SecurityContextRepository 设为 NullSecurityContextRepository 对象。

3. 登录状态保存

过滤器链中有过滤器 SessionManagementFilter,它会当前安全上下文中有 Authentication 的情况下,调用 SecurityContextRepositorysaveContext() 方法保存安全上下文。

如果 SecurityContextRepositoryHttpSessionSecurityContextRepository 对象,会在 Session 中保存安全上下文;如果是 NullSecurityContextRepository 对象,则什么也不会做。

4. 登录状态读取

当接收到请求时,过滤器链会对请求做处理,其中有过滤器 SecurityContextPersistenceFilter,它会调用 SecurityContextRepositoryloadContext() 方法加载安全上下文。

如果 SecurityContextRepositoryHttpSessionSecurityContextRepository 对象,会从 Session 中获取安全上下文并返回;如果是 NullSecurityContextRepository 对象,会创造一个新的安全上下文并返回。

七、源码阅读 - AuthenticationProvider

1. AuthenticationProvider

AuthenticationProvider 是一个接口,它包含两个方法,代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface AuthenticationProvider {

/**
* 对Authentication实例进行校验
* 如果校验成功,返回包含完整信息的Authentication实例
* 如果校验失败,则返回null或抛出异常
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;

/**
* 返回该类是否支持校验对应的Authentication实例
*/
boolean supports(Class<?> authentication);

}

2. AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider 是一个抽象类,它实现了 AuthenticationProvider 接口,描述了验证 Authentication 实现类 UsernamePasswordAuthenticationToken 的通用方法:

  • 根据用户名查询完整的 UserDetails
  • 将完整的 UserDetails 与 Authentication 实例中的信息比对,进行身份验证
  • 若身份验证通过,则将完整的用户信息包装成 Authentication 对象返回

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
···
// 调用 retrieveUser(),根据用户名获取完整的用户信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
···
// 校验用户
···
// 返回包含完整信息的Authentication对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}

/**
* 抽象方法,根据用户名获取完整的用户信息
*/
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;

}

3. DaoAuthenticationProvider

DaoAuthenticationProvider 类继承自 AbstractUserDetailsAuthenticationProvider 抽象类,并实现了retrieveUser() 方法,该方法的作用是根据请求验证的部分信息,从数据库查询完整的用户信息。

DaoAuthenticationProvider 的代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

···

@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication){
// 通过UserDetailsService和用户名查找完整的用户信息,并返回
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
return loadedUser;
}

···

}

4. AbstractUserDetailsAuthenticationProvider

其中有 authenticate() 方法,它将会调用retrieveUser() 方法获取完整用户信息并进行校验。

八、源码阅读 - 默认验证类

1. 默认验证类

DaoAuthenticationProvider 是 Spring Security 中的默认验证类。

2. 未配置时

(1) 说明

如果没有配置 AuthenticationProvider 验证类,但能找到 userDetailsService 时,默认验证类会被自动添加。

Spring Security 自己初始化了 userDetailsService 这个 Bean。

(2) AuthenticationConfiguration 类

AuthenticationConfiguration 类是 Spring Security 的认证配置类。

AuthenticationConfiguration 类的启用方式是:

  • Spring Security 配置类需要用 @EnableWebSecurity 注解
  • @EnableWebSecurity 注解中引入了 @EnableGlobalAuthentication 注解
  • @EnableGlobalAuthentication 注解中使用了 @Import(AuthenticationConfiguration.class) 引入了 AuthenticationConfiguration 类

AuthenticationConfiguration 类中有各种初始化方法,其中有方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AuthenticationConfiguration {

···

@Bean
public static InitializeUserDetailsBeanManagerConfigurer initializeUserDetailsBeanManagerConfigurer(
ApplicationContext context) {
return new InitializeUserDetailsBeanManagerConfigurer(context);
}

@Autowired(required = false)
public void setGlobalAuthenticationConfigurers(List<GlobalAuthenticationConfigurerAdapter> configurers) {
configurers.sort(AnnotationAwareOrderComparator.INSTANCE);
this.globalAuthConfigurers = configurers;
}

···

}
  • initializeUserDetailsBeanManagerConfigurer() 方法使用 @Bean 注解,因此它将会在在类初始化后执行一次,并将返回值(即 InitializeAuthenticationProviderBeanManagerConfigurer 实例)作为 Bean 交由 Spring 托管
  • setGlobalAuthenticationConfigurers(List<GlobalAuthenticationConfigurerAdapter> configurers) 方法使用 @Autowired 注解,因此 Spring 会将所有 GlobalAuthenticationConfigurerAdapter 及其子类的实例注入到 List 之中,该 List 会在排序后被复制给类的成员变量 globalAuthConfigurers,等待后续调用

简而言之,AuthenticationConfiguration 类会实例化并应用 InitializeUserDetailsBeanManagerConfigurer。

(3) InitializeUserDetailsBeanManagerConfigurer 类

具体请看:

org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer

它的作用是:

添加 InitializeUserDetailsManagerConfigurer。

(4) InitializeUserDetailsManagerConfigurer

InitializeUserDetailsManagerConfigurer 类是 InitializeUserDetailsBeanManagerConfigurer 类的内联类,它继承了 GlobalAuthenticationConfigurerAdapter 类,并重写了 configure() 方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 如果添加了 AuthenticationProvider,或设置了非空的 parent AuthenticationManager,则什么也不做
if (auth.isConfigured()) {
return;
}
// 如果没有配置 userDetailsService,则什么也不做
UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class);
if (userDetailsService == null) {
return;
}
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
// 实例化 DaoAuthenticationProvider 并添加
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
if (passwordEncoder != null) {
provider.setPasswordEncoder(passwordEncoder);
}
if (passwordManager != null) {
provider.setUserDetailsPasswordService(passwordManager);
}
provider.afterPropertiesSet();
auth.authenticationProvider(provider);
}

/**
* 获取 Bean,没有时返回 null
*/
private <T> T getBeanOrNull(Class<T> type) {
···
}

}

该类的作用时,

  • 首先判断有没有配置 AuthenticationProvider 验证类,没有则继续
  • 向 Spring 获取 userDetailsService 的 Bean,有则继续
  • 实例化 DaoAuthenticationProvider
  • 向 Spring 获取 passwordEncoder 密码加密器(用于将明文密码加密)和 userDetailsPasswordManager 密码修改器(用于对存储的用户信息中的密码进行修改),如果有则应用到 DaoAuthenticationProvider 之上
  • 添加 DaoAuthenticationProvider

3. 配置 UserDetailsService 时

(1) 说明

如果配置了 UserDetailService,则 DaoAuthenticationProvider 会被添加,并可以被配置。

(2) userDetailsService()

如果希望配置 UserDetailsService,需要调用 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<>(userDetailsService));
}

接收一个 UserDetailsService 的实例,返回一个 DaoAuthenticationConfigurer 用于配置。

(3) DaoAuthenticationConfigurer

DaoAuthenticationConfigurer 继承自 AbstractDaoAuthenticationConfigurer,代码如下:

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

public DaoAuthenticationConfigurer(U userDetailsService) {
super(userDetailsService);
}

}

此时,就可以通过 DaoAuthenticationConfigurer 对 DaoAuthenticationProvider 进行配置。

(3) AbstractDaoAuthenticationConfigurer

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public abstract class AbstractDaoAuthenticationConfigurer<B extends ProviderManagerBuilder<B>, C extends AbstractDaoAuthenticationConfigurer<B, C, U>, U extends UserDetailsService>
extends UserDetailsAwareConfigurer<B, U> {

private DaoAuthenticationProvider provider = new DaoAuthenticationProvider();

AbstractDaoAuthenticationConfigurer(U userDetailsService) {
this.userDetailsService = userDetailsService;
this.provider.setUserDetailsService(userDetailsService);
if (userDetailsService instanceof UserDetailsPasswordService) {
this.provider.setUserDetailsPasswordService((UserDetailsPasswordService) userDetailsService);
}
}

···

@Override
public void configure(B builder) throws Exception {
this.provider = postProcess(this.provider);
builder.authenticationProvider(this.provider);
}

···

}
Copy

它的构造器会将 UserDetailsService 应用到 DaoAuthenticationProvider 中;

它的 configure() 方法将会添加 DaoAuthenticationProvider 。

参考