前言
本文是对 Spring Security Core 4.0.4 Release 进行源码分析的系列文章之一;
本文主要描述 Spring Security 的核心 Services,
本文为笔者的原创作品,转载需注明出处;
类图
为了便于理解,笔者画了如下的一张类图,涵盖了 Spring Security 相关的 Core Services;如下,
概述
两大核心 Services
AuthenticationManager
从 Spring Security 源码分析二:Core Components 核心元素,我们知道,该对象提供了认证方法的入口;并且它只实现了一个用于认证的方法,接收 Authentication 对象作为验证的参数;1
2Authentication authenticate(Authentication authentication)
throws AuthenticationException;ProviderManager
它是 AuthenticationManager 的一个实现类,提供了基本的认证逻辑和方法;不过为了便于灵活扩展,它包含了一个 List<AuthenticationProvider> 对象,通过 AuthenticationProvider 接口来扩展出不同的认证提供者;AuthenticationProvider
正如 #2 所描述的那样,该对象主要用来为 ProviderManager 扩展出不同的认证提供者既 Providers,从类图中,我们可以清晰的看到,扩展出的 Providers 有 AnonymousAuthenticationProvider、DaoAuthenticationProvider、LdapAuthenticationProvider… 等
验证逻辑
从类图中我们不难发现其与之验证的相关逻辑,AuthenticationManager 接收 Authentication 对象作为参数,并通过 authenticate(Authentication) 方法对其进行验证;下半部分黄色部分,提供了一系列的类和功能用来支撑对 Authentication 对象的验证动作;上半部分,介绍了 Authentication 的组成,该部分主要是将用户输入的用户名和密码进行封装,封装成 Authentication 对象,并供给 AuthenticationManager 进行验证;验证完成以后将返回一个认证成功的 Authentication 对象;
Authentication
1 | public interface Authentication extends Principal, Serializable { |
ProviderManager
ProviderManager
是AuthenticationManager
的实现类,提供了基本认证实现逻辑和流程;我们来看看它的核心源码片段,
1 | /* |
ProviderManager
通过实现AuthenticationManager
接口方法 authenticate() 实现验证逻辑,主要流程包含三个方面,
- 遍历所有的 Providers,然后依次执行该 Provider 的验证方法
- 如果某一个 Provider 验证成功,则跳出循环不再执行后续的验证;
- 如果验证成功,会将返回的 result 既 Authentication 对象进一步封装为 Authentication Token;
比如 UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken 等;这些 Authentication Token 也都继承自 Authentication 对象;
- 如果 #1 没有任何一个 Provider 验证成功,则试图使用其 parent Authentication Manager 进行验证;
- 是否需要擦除密码等敏感信息;
AuthenticationProvider
从小节 ProviderManager 的源码分析中我们已经知道,Provider Manager 通过 AuthenticationProvider 扩展出更多的验证提供的方式;而 AuthenticationProvider 本身也就是一个接口,由更多的实现类实现;类图中,我画出了部分的 Provider 实现类,比如 AnonymousAuthenticationProvider、AbstractUserDetailsAuthenticationProvider 以及 AbstractLdapAuthenticationProvider,当然实际实现类还有很多很多;不过这里不得不提及的是这个 DaoAuthenticationProvider
,它算是 Spring Security 用得最广泛的一个 Provider 了,里面提供了各种各样的用户信息来源的方式,比如通过数据库,内存或者是 LDAP;下面,笔者就 DaoAuthenticationProvider
作进一步的阐述;
DaoAuthenticationProvider Domain
首先,来回顾下什么是 DAO,英文全名 Data Access Object,数据访问对象;ok,那么顾名思义,DaoAuthenticationProvider
就是试图为一切需要被验证的 数据访问对象 提供这么一种 Authentication Provider;回到类图,从中,可以看到,它包含一个属性UserDetailService
,它是一个接口,该接口异常的重要,正是由它扩展出了多种多样的不同的数据访问来源;
DaoAuthenticationProvider
该 Provider 算是 Spring Scruity Core 中最核心的一个 Authentication Provider,对所有需要被验证的 Data Access Object 提供验证的基本方法和入口,该 Provider 继承自AbstractUserDetailsAuthenticationProvider
;
DaoAuthenticationProvider
DaoAuthenticationProvider
继承自AbstractUserDetailsAuthenticationProvider
主要做了三件事情,
对用户身份信息进行加密操作;主要是传入一个
PasswordEncoder
对象1
private PasswordEncoder passwordEncoder;
实现了 AbstractUserDetailsAuthenticationProvider 预留的两个扩展点,
获取用户信息的扩展点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19private UserDetailsService userDetailsService;
...
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
....
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}可见,主要是通过注入
UserDetailsService
接口对象,并调用其接口方法 loadUserByUsername(String username) 获取得到相关的用户信息;正如类图中我们所看到的那样,UserDetailsService
接口非常重要,接入了 JdbcDaoImpl,InMemoryUserDetailsManager 等等用户来源接口实现类;实现相关的 additionalAuthenticationChecks 的额外验证方法;
该抽象方法是 AbstractUserDetailsAuthenticationProvider 提供给子类的可扩展的核心入口方法,我们看看 DaoAuthenticationProvider 是怎么做的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
28protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
presentedPassword, salt)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}逻辑非常明了,如果设置了 PasswordEncoder 那么就对当前用户输入的密码进行 Hash 与那算操作,然后将 Hash 结果与当前用户的密码进行比对;
AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider
为DaoAuthenticationProvider
提供了基本的认证方法;先看看它的关键部分源码,
1 | public Authentication authenticate(Authentication authentication) |
AbstractUserDetailsAuthenticationProvider
主要实现了AuthenticationProvider
的接口方法 authenticate 并提供了相关的验证逻辑;笔者将绝大部分的不相干的代码都删除了,包括其中的缓存机制,留下了最核心的几行代码,下面,我们分别来看看这几步的逻辑,
获取用户
AbstractUserDetailsAuthenticationProvider
定义了一个抽象的方法1
2
3protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;通过用户名去获得用户的信息并作为
UserDetails
对象进行返回;- 三步验证方法
- preAuthenticationChecks
提供了对用户基本信息的一些默认的前置验证逻辑,包括,用户账户是否被锁定,是否是 enabled 的状态以及用户账户是否过期等逻辑的验证; - additionalAuthenticationChecks
这是一个扩展点
,该方法是一个抽象方法,必须由子类进行实现, - postAuthenticationChecks
提供了对用户基本信息的一些默认的后置验证逻辑,默认实现很简单,就是对用户的 Credential 既密码进行判断,判断其是否过期。
- preAuthenticationChecks
- 最后,将已通过验证的用户信息封装成 UsernamePasswordAuthenticationToken 对象并返回;该对象封装了用户的身份信息,以及相应的权限信息,相关源码如下,
1
2
3
4
5
6
7
8
9
10
11
12
13protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
UserDetailsService
UserDetailsService
是一个接口,提供了一个方法
1 | public interface UserDetailsService { |
可见,通过用户名 username 调用方法 loadUserByUsername 返回了一个UserDetails
接口对象;
Spring 为UserDetailsService
默认提供了一个实现类 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl
JdbcDaoImpl
该实现类非常的简单,内部逻辑就是如何通过 JDBC 的方式,通过用户名 username 查询得到一个用户的信息,并将其封装为 UserDetails 对象,JdbcDaoImpl
是将其封装为一个 org.springframework.security.core.userdetails.User
对象;
UserDetails
UserDetails
接口就是对基本用户信息进行一些常规的定义,比如,该用户的用户名、密码是什么,账户是否已经过期等等;而其相关的实现类,比如 User、CustomUserDetails、LdapUserDetails 提供了更多的实现细节;
UserDetailsManager
UserDetailsManager
继承自UserDetailService
,扩展除了更多的对用户进行管理
的方法,如下所述,
1 | /** |
可以看到UserDetailsManager
提供了对用户信息的增
、删
、查
、改
等方法;其对应的实现类分别有 JdbcUserDetailsManager、InMemoryUserDetailsManager 以及 LdapUserDetailsManager;
JdbcUserDetailsManager
该实现类主要是提供基于JDBC
对 User 进行增
、删
、查
、改
的方法,笔者就一些核心代码摘录如下,
1 | public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager, |
InMemoryUserDetailsManager
该实现类主要是提供基于内存
对 User 进行增
、删
、查
、改
的方法,笔者就一些核心代码摘录如下,
1 | public class InMemoryUserDetailsManager implements UserDetailsManager { |
通过 Map 对象,将用户存储在内存中;
LdapUserDetailsManager
该实现类主要是提供基于Ldap
对 User 进行增
、删
、查
、改
的方法,笔者就一些核心代码摘录如下,
总结
从这里我们就可以清晰的看到,UserDetailsService
接口作为桥梁,是DaoAuthenticationProvier
与特定用户信息来源进行解耦的地方,UserDetailsService
由UserDetails
和UserDetailsManager
所构成;UserDetails
和UserDetailsManager
各司其责,一个是对基本用户信息进行封装,一个是对基本用户信息进行管理;
特别注意
,UserDetailsService
、UserDetails
以及UserDetailsManager
都是可被用户自定义的扩展点,我们可以继承这些接口提供自己的读取用户来源和管理用户的方法,比如我们可以自己实现一个 与特定 ORM 框架,比如 Mybatis 或者 Hibernate,相关的UserDetailsService
和UserDetailsManager
;
配置样例
In Memory Authentication
自定义 DaoAuthenticationProvider 实例1
2
3
4
5<bean id="daoAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryUserDetailsManager"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>
这样,我们自定义了一个使用 InMemoryUserDetailsManager 和 passwordEncoder 的 DaoAuthenticationProvider 实例;
使用 namespace 配置
可以直接使用 namespace 中所定义的user-service
1
2
3
4<user-service id="userDetailsService">
<user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="bobspassword" authorities="ROLE_USER" />
</user-service>
这里定义了一个 UserDetailsService 实例,并在 namespace 的解析过程中注册到 DaoAuthenticationProvider 的容器实例对象中;
JdbcDaoImpl
1 | <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> |