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) UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken 是 Authentication 的一个实现类,它旨在简单地呈现用户名和密码。
3. 验证管理器
在 Spring Security 中,ProviderManager 作为验证管理器。
它将会被过滤器链中的 UsernamePasswordAuthenticationFilter 调用,用于对用户的登录请求做校验。
它的作用是:
- 遍历 AuthenticationProvider 集合
- 判断 AuthenticationProvider 是否支持校验 Authentication 实例
- 如果支持,则调用 AuthenticationProvider,将 Authentication 实例传入,由 AuthenticationProvider 进行校验
- 一旦校验通过,则将 AuthenticationProvider 返回的完整的 Authentication 实例返回
代码大致如下:
1 |
|
三、登录状态保持
如果开启了 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. UserDetailsService
UserDetailsService 的作用是:
被 AuthenticationProvider 验证类(通常是 DaoAuthenticationProvider 验证类)调用,返回用户的完整信息,以供 AuthenticationProvider 验证类进行比对。
3. 用户信息的存储
(1) 存储于内存中
可以在配置类中创建用户到内存,Spring Security 会在用户请求登录时根据请求中的部分信息(通常是用户名)查找完整的用户信息,进行比对。
1
2
3
4auth
.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 的情况下,调用 SecurityContextRepository
的 saveContext()
方法保存安全上下文。
如果 SecurityContextRepository
是 HttpSessionSecurityContextRepository
对象,会在 Session 中保存安全上下文;如果是 NullSecurityContextRepository
对象,则什么也不会做。
4. 登录状态读取
当接收到请求时,过滤器链会对请求做处理,其中有过滤器 SecurityContextPersistenceFilter
,它会调用 SecurityContextRepository
的 loadContext()
方法加载安全上下文。
如果 SecurityContextRepository
是 HttpSessionSecurityContextRepository
对象,会从 Session 中获取安全上下文并返回;如果是 NullSecurityContextRepository
对象,会创造一个新的安全上下文并返回。
七、源码阅读 - AuthenticationProvider
1. AuthenticationProvider
AuthenticationProvider 是一个接口,它包含两个方法,代码大致如下:
1 |
|
2. AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider 是一个抽象类,它实现了 AuthenticationProvider 接口,描述了验证 Authentication 实现类 UsernamePasswordAuthenticationToken 的通用方法:
- 根据用户名查询完整的 UserDetails
- 将完整的 UserDetails 与 Authentication 实例中的信息比对,进行身份验证
- 若身份验证通过,则将完整的用户信息包装成 Authentication 对象返回
代码如下:
1 |
|
3. DaoAuthenticationProvider
DaoAuthenticationProvider 类继承自 AbstractUserDetailsAuthenticationProvider 抽象类,并实现了retrieveUser()
方法,该方法的作用是根据请求验证的部分信息,从数据库查询完整的用户信息。
DaoAuthenticationProvider 的代码大致如下:
1 |
|
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 |
|
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 |
|
该类的作用时,
- 首先判断有没有配置 AuthenticationProvider 验证类,没有则继续
- 向 Spring 获取 userDetailsService 的 Bean,有则继续
- 实例化 DaoAuthenticationProvider
- 向 Spring 获取 passwordEncoder 密码加密器(用于将明文密码加密)和 userDetailsPasswordManager 密码修改器(用于对存储的用户信息中的密码进行修改),如果有则应用到 DaoAuthenticationProvider 之上
- 添加 DaoAuthenticationProvider
3. 配置 UserDetailsService 时
(1) 说明
如果配置了 UserDetailService,则 DaoAuthenticationProvider 会被添加,并可以被配置。
(2) userDetailsService()
如果希望配置 UserDetailsService,需要调用 userDetailsService()
方法,该方法如下:
1 |
|
接收一个 UserDetailsService 的实例,返回一个 DaoAuthenticationConfigurer 用于配置。
(3) DaoAuthenticationConfigurer
DaoAuthenticationConfigurer 继承自 AbstractDaoAuthenticationConfigurer,代码如下:
1 |
|
此时,就可以通过 DaoAuthenticationConfigurer 对 DaoAuthenticationProvider 进行配置。
(3) AbstractDaoAuthenticationConfigurer
代码如下:
1 |
|
它的构造器会将 UserDetailsService 应用到 DaoAuthenticationProvider 中;
它的 configure()
方法将会添加 DaoAuthenticationProvider 。