Spring Security 源码分析三:Core Services 核心服务

前言

本文是对 Spring Security Core 4.0.4 Release 进行源码分析的系列文章之一;

本文主要描述 Spring Security 的核心 Services,

本文为笔者的原创作品,转载需注明出处;

类图

为了便于理解,笔者画了如下的一张类图,涵盖了 Spring Security 相关的 Core Services;如下,

概述

两大核心 Services

  1. AuthenticationManager
    Spring Security 源码分析二:Core Components 核心元素,我们知道,该对象提供了认证方法的入口;并且它只实现了一个用于认证的方法,接收 Authentication 对象作为验证的参数;

    1
    2
    Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

    ProviderManager
    它是 AuthenticationManager 的一个实现类,提供了基本的认证逻辑和方法;不过为了便于灵活扩展,它包含了一个 List<AuthenticationProvider> 对象,通过 AuthenticationProvider 接口来扩展出不同的认证提供者;

  2. AuthenticationProvider
    正如 #2 所描述的那样,该对象主要用来为 ProviderManager 扩展出不同的认证提供者既 Providers,从类图中,我们可以清晰的看到,扩展出的 Providers 有 AnonymousAuthenticationProvider、DaoAuthenticationProvider、LdapAuthenticationProvider… 等

验证逻辑

从类图中我们不难发现其与之验证的相关逻辑,AuthenticationManager 接收 Authentication 对象作为参数,并通过 authenticate(Authentication) 方法对其进行验证;下半部分黄色部分,提供了一系列的类和功能用来支撑对 Authentication 对象的验证动作;上半部分,介绍了 Authentication 的组成,该部分主要是将用户输入的用户名和密码进行封装,封装成 Authentication 对象,并供给 AuthenticationManager 进行验证;验证完成以后将返回一个认证成功的 Authentication 对象;

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public interface Authentication extends Principal, Serializable {

Collection<? extends GrantedAuthority> getAuthorities();

/**
* The credentials that prove the principal is correct. This is usually a password,
* but could be anything relevant to the <code>AuthenticationManager</code>. Callers
* are expected to populate the credentials.
*
* @return the credentials that prove the identity of the <code>Principal</code>
*/
Object getCredentials();

/**
* Stores additional details about the authentication request. These might be an IP
* address, certificate serial number etc.
*
* @return additional details about the authentication request, or <code>null</code>
* if not used
*/
Object getDetails();

/**
* The identity of the principal being authenticated. In the case of an authentication
* request with username and password, this would be the username. Callers are
* expected to populate the principal for an authentication request.
* <p>
* The <tt>AuthenticationManager</tt> implementation will often return an
* <tt>Authentication</tt> containing richer information as the principal for use by
* the application. Many of the authentication providers will create a
* {@code UserDetails} object as the principal.
*
* @return the <code>Principal</code> being authenticated or the authenticated
* principal after authentication.
*/
Object getPrincipal();

/**
* Used to indicate to {@code AbstractSecurityInterceptor} whether it should present
* the authentication token to the <code>AuthenticationManager</code>. Typically an
* <code>AuthenticationManager</code> (or, more often, one of its
* <code>AuthenticationProvider</code>s) will return an immutable authentication token
* after successful authentication, in which case that token can safely return
* <code>true</code> to this method. Returning <code>true</code> will improve
* performance, as calling the <code>AuthenticationManager</code> for every request
* will no longer be necessary.
* <p>
* For security reasons, implementations of this interface should be very careful
* about returning <code>true</code> from this method unless they are either
* immutable, or have some way of ensuring the properties have not been changed since
* original creation.
*
* @return true if the token has been authenticated and the
* <code>AbstractSecurityInterceptor</code> does not need to present the token to the
* <code>AuthenticationManager</code> again for re-authentication.
*/
boolean isAuthenticated();

/**
* See {@link #isAuthenticated()} for a full description.
* <p>
* Implementations should <b>always</b> allow this method to be called with a
* <code>false</code> parameter, as this is used by various classes to specify the
* authentication token should not be trusted. If an implementation wishes to reject
* an invocation with a <code>true</code> parameter (which would indicate the
* authentication token is trusted - a potential security risk) the implementation
* should throw an {@link IllegalArgumentException}.
*
* @param isAuthenticated <code>true</code> if the token should be trusted (which may
* result in an exception) or <code>false</code> if the token should not be trusted
*
* @throws IllegalArgumentException if an attempt to make the authentication token
* trusted (by passing <code>true</code> as the argument) is rejected due to the
* implementation being immutable or implementing its own alternative approach to
* {@link #isAuthenticated()}
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

ProviderManager

ProviderManagerAuthenticationManager的实现类,提供了基本认证实现逻辑和流程;我们来看看它的核心源码片段,

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*
* @param authentication the authentication request object.
* @return a fully authenticated object including credentials.
* @throws AuthenticationException if authentication fails.
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
...
Authentication result = null;
...

// 1. 遍历所有的 Providers 并分别调用它们的验证方法 authenticate
for (AuthenticationProvider provider : getProviders()) {

...

try {

result = provider.authenticate(authentication);

if( result != null ){
// 1.1 生成各种各样的 Authentication Tokens
copyDetails(authentication, result);
break;
}

}
catch(..){
....
}
}

// 2. 若 #1 没有验证成功,试试用父类的 AuthencationManager 进行验证
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch(..){
....
}
}

// 3. 是否需要擦除密码等敏感信息;
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {

((CredentialsContainer) result).eraseCredentials();
}

eventPublisher.publishAuthenticationSuccess(result);
return result;
}

}

ProviderManager通过实现AuthenticationManager接口方法 authenticate() 实现验证逻辑,主要流程包含三个方面,

  1. 遍历所有的 Providers,然后依次执行该 Provider 的验证方法
    • 如果某一个 Provider 验证成功,则跳出循环不再执行后续的验证;
    • 如果验证成功,会将返回的 result 既 Authentication 对象进一步封装为 Authentication Token;
      比如 UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken 等;这些 Authentication Token 也都继承自 Authentication 对象;
  2. 如果 #1 没有任何一个 Provider 验证成功,则试图使用其 parent Authentication Manager 进行验证;
  3. 是否需要擦除密码等敏感信息;

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主要做了三件事情,

  1. 对用户身份信息进行加密操作;主要是传入一个PasswordEncoder对象

    1
    private PasswordEncoder passwordEncoder;
  2. 实现了 AbstractUserDetailsAuthenticationProvider 预留的两个扩展点,

    • 获取用户信息的扩展点

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      private 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接口非常重要,接入了 JdbcDaoImplInMemoryUserDetailsManager 等等用户来源接口实现类;

    • 实现相关的 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
      28
      protected 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

AbstractUserDetailsAuthenticationProviderDaoAuthenticationProvider提供了基本的认证方法;先看看它的关键部分源码,

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
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {

// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();

...
// 1. 获取用户
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
// 2.1 pre-authenticate
preAuthenticationChecks.check(user);
// 2.2 additional-authenticate
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
...
// 2.3 post-authenticate
postAuthenticationChecks.check(user);

...

Object principalToReturn = user;

if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}

// 3. 封装通过验证的用户信息
return createSuccessAuthentication(principalToReturn, authentication, user);
}

AbstractUserDetailsAuthenticationProvider主要实现了AuthenticationProvider的接口方法 authenticate 并提供了相关的验证逻辑;笔者将绝大部分的不相干的代码都删除了,包括其中的缓存机制,留下了最核心的几行代码,下面,我们分别来看看这几步的逻辑,

  1. 获取用户
    AbstractUserDetailsAuthenticationProvider定义了一个抽象的方法

    1
    2
    3
    protected abstract UserDetails retrieveUser(String username,
    UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException;

    通过用户名去获得用户的信息并作为UserDetails对象进行返回;

  2. 三步验证方法
    • preAuthenticationChecks
      提供了对用户基本信息的一些默认的前置验证逻辑,包括,用户账户是否被锁定,是否是 enabled 的状态以及用户账户是否过期等逻辑的验证;
    • additionalAuthenticationChecks
      这是一个扩展点,该方法是一个抽象方法,必须由子类进行实现,
    • postAuthenticationChecks
      提供了对用户基本信息的一些默认的后置验证逻辑,默认实现很简单,就是对用户的 Credential 既密码进行判断,判断其是否过期。
  3. 最后,将已通过验证的用户信息封装成 UsernamePasswordAuthenticationToken 对象并返回;该对象封装了用户的身份信息,以及相应的权限信息,相关源码如下,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    protected 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
2
3
4
public interface UserDetailsService {

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

可见,通过用户名 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
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
39
/**
* An extension of the {@link UserDetailsService} which provides the ability to create new
* users and update existing ones.
*
* @author Luke Taylor
* @since 2.0
*/
public interface UserDetailsManager extends UserDetailsService {

/**
* Create a new user with the supplied details.
*/
void createUser(UserDetails user);

/**
* Update the specified user.
*/
void updateUser(UserDetails user);

/**
* Remove the user with the given login name from the system.
*/
void deleteUser(String username);

/**
* Modify the current user's password. This should change the user's password in the
* persistent user repository (datbase, LDAP etc).
*
* @param oldPassword current password (for re-authentication if required)
* @param newPassword the password to change to
*/
void changePassword(String oldPassword, String newPassword);

/**
* Check if a user with the supplied login name exists in the system.
*/
boolean userExists(String username);

}

可以看到UserDetailsManager提供了对用户信息的等方法;其对应的实现类分别有 JdbcUserDetailsManager、InMemoryUserDetailsManager 以及 LdapUserDetailsManager;

JdbcUserDetailsManager

该实现类主要是提供基于JDBC对 User 进行的方法,笔者就一些核心代码摘录如下,

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager,
GroupManager {
// ~ Static fields/initializers
// =====================================================================================

// UserDetailsManager SQL
public static final String DEF_CREATE_USER_SQL = "insert into users (username, password, enabled) values (?,?,?)";
public static final String DEF_DELETE_USER_SQL = "delete from users where username = ?";
public static final String DEF_UPDATE_USER_SQL = "update users set password = ?, enabled = ? where username = ?";
public static final String DEF_INSERT_AUTHORITY_SQL = "insert into authorities (username, authority) values (?,?)";
public static final String DEF_DELETE_USER_AUTHORITIES_SQL = "delete from authorities where username = ?";
public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";
public static final String DEF_CHANGE_PASSWORD_SQL = "update users set password = ? where username = ?";

......

private AuthenticationManager authenticationManager;

......

public void createUser(final UserDetails user) {
validateUserDetails(user);
getJdbcTemplate().update(createUserSql, new PreparedStatementSetter() {
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setBoolean(3, user.isEnabled());
}

});

if (getEnableAuthorities()) {
insertUserAuthorities(user);
}
}

public void updateUser(final UserDetails user) {
validateUserDetails(user);
getJdbcTemplate().update(updateUserSql, new PreparedStatementSetter() {
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, user.getPassword());
ps.setBoolean(2, user.isEnabled());
ps.setString(3, user.getUsername());
}
});

if (getEnableAuthorities()) {
deleteUserAuthorities(user.getUsername());
insertUserAuthorities(user);
}

userCache.removeUserFromCache(user.getUsername());
}

......

}
InMemoryUserDetailsManager

该实现类主要是提供基于内存对 User 进行的方法,笔者就一些核心代码摘录如下,

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
public class InMemoryUserDetailsManager implements UserDetailsManager {
protected final Log logger = LogFactory.getLog(getClass());

private final Map<String, MutableUserDetails> users = new HashMap<String, MutableUserDetails>();

private AuthenticationManager authenticationManager;

......

public void createUser(UserDetails user) {
Assert.isTrue(!userExists(user.getUsername()));

users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}

public void deleteUser(String username) {
users.remove(username.toLowerCase());
}

public void updateUser(UserDetails user) {
Assert.isTrue(userExists(user.getUsername()));

users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}

......

}

通过 Map 对象,将用户存储在内存中;

LdapUserDetailsManager

该实现类主要是提供基于Ldap对 User 进行的方法,笔者就一些核心代码摘录如下,

总结

从这里我们就可以清晰的看到,UserDetailsService接口作为桥梁,是DaoAuthenticationProvier与特定用户信息来源进行解耦的地方,UserDetailsServiceUserDetailsUserDetailsManager所构成;UserDetailsUserDetailsManager各司其责,一个是对基本用户信息进行封装,一个是对基本用户信息进行管理;

特别注意UserDetailsServiceUserDetails以及UserDetailsManager都是可被用户自定义的扩展点,我们可以继承这些接口提供自己的读取用户来源和管理用户的方法,比如我们可以自己实现一个 与特定 ORM 框架,比如 Mybatis 或者 Hibernate,相关的UserDetailsServiceUserDetailsManager

配置样例

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>

这样,我们自定义了一个使用 InMemoryUserDetailsManagerpasswordEncoder 的 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
2
3
4
5
6
7
8
9
10
11
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>

<bean id="userDetailsService"
class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>

时序图