一、OverView

​ 这一小节整理一下前面几节的内容,主要完整的捋一遍 Spring Security 的登录过程。为了有一个 Architecture First 的印象,先将其中关键的类和接口列出,如果看到后面发现有印象,就可以跳到前面来看一下:

  • SecurityContextHolder

  • Authentication

  • Token 类

    • UsernamePasswordAuthenticationToken
    • AbstractAuthenticationToken
  • Provider 和 Manger 类

    • AuthenticationManager
    • ProviderManager
    • AuthenticationProvider
    • DaoAuthenticationProvider
  • UserDetailsUserDetailsService

主要可以参考一下这张图:

image-20201013133113040

图源:Spring Security(一)–Architecture Overview

二、DeepLearning

2.1 Authentication

先看一下 Authentication 的相关实现类

image-20201013084517460

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();

Object getCredentials();

Object getDetails();

Object getPrincipal();

boolean isAuthenticated();

void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
  • getAuthorities():权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是代表权限信息的一系列字符串。
  • getCredentials():密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
  • getDetails():细节信息,web 应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的 ip 地址和 sessionId 的值。
  • getPrincipal():最重要的身份信息,大部分情况下返回的是 UserDetails 接口的实现类,也是框架中的常用接口之一。UserDetails 接口将会在下面的小节重点介绍。

在实现类中非常重要的就是 UsernamePasswordAuthenticationToken ,再看一下其中的属性和方法

image-20201013090502537

以及它的父类:AbstractAuthenticationToken

image-20201013092219226

2.2 UsernamePasswordAuthenticationFilter

在 Spring Security 中,认证与授权的相关校验都是在一系列的过滤器链中完成的,在这一系列的过滤器链中,和认证相关的过滤器就是 UsernamePasswordAuthenticationFilter:

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
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}

@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}

@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
}

其中:UsernamePasswordAuthenticationFilter 是继承 AbstractAuthenticationProcessingFilter

  • 通过 obtainUsername 和 obtainPassword 方法提取出请求里边的用户名/密码出来,提取方式就是 request.getParameter
  • 获取到请求里传递来的用户名/密码之后,接下来就构造一个 UsernamePasswordAuthenticationToken 对象,传入 username 和 password,username 对应了 UsernamePasswordAuthenticationToken 中的 principal 属性,而 password 则对应了它的 credentials 属性
  • 接下来 setDetails 方法给 details 属性赋值,UsernamePasswordAuthenticationToken 本身是没有 details 属性的,这个属性在它的父类 AbstractAuthenticationToken 中。details 是一个对象,这个对象里边放的是 WebAuthenticationDetails 实例,该实例主要描述了两个信息,请求的 remoteAddress 以及请求的 sessionId
  • 最后调用 authenticate 方法去做校验了

仔细来细说一下上面代码中的最后一个方法中的校验:authenticate

校验操作首先要获取到一个 AuthenticationManager:

image-20201013100826654

这里拿到的是 ProviderManager ,所以接下来我们就进入到 ProviderManager 的 authenticate 方法中:

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
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();

for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}

if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}

try {
result = provider.authenticate(authentication);

if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}

if (result == null && parent != null) {
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}

if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}

if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}

......

throw lastException;
}
  • 首先获取 authentication 的 Class,判断当前 provider 是否支持该 authentication
  • 如果支持,则调用 provider 的 authenticate 方法开始做校验,校验完成后,会返回一个新的 Authentication
  • 这里的 provider 可能有多个,如果 provider 的 authenticate 方法没能正常返回一个 Authentication,则调用 provider 的 parent 的 authenticate 方法继续校验
  • copyDetails 方法则用来把旧的 Token 的 details 属性拷贝到新的 Token 中来
  • 接下来会调用 eraseCredentials 方法擦除凭证信息,也就是你的密码,这个擦除方法比较简单,就是将 Token 中的 credentials 属性置空
  • 最后通过 publishAuthenticationSuccess 方法将登录成功的事件广播出去

在具体说一下调用 provider 的 authenticate 方法的过程:

  • 在 for 循环中,第一次拿到的 provider 是一个 AnonymousAuthenticationProvider,这个 provider 压根就不支持 UsernamePasswordAuthenticationToken,也就是会直接在 provider.supports 方法中返回 false,结束 for 循环
  • 然后会进入到下一个 if 中,直接调用 parent 的 authenticate 方法进行校验。而 parent 就是 ProviderManager,所以会再次回到这个 authenticate 方法中。再次回到 authenticate 方法中,provider 也变成了 DaoAuthenticationProvider,这个 provider 是支持 UsernamePasswordAuthenticationToken 的
  • 所以会顺利进入到该类的 authenticate 方法去执行,而 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider 并且没有重写 authenticate 方法,所以 我们最终来到 AbstractUserDetailsAuthenticationProvider#authenticate 方法中:
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
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));

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

boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);

if (user == null) {
cacheWasUsed = false;

try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");

if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}

Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}

try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}

postAuthenticationChecks.check(user);

if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}

Object principalToReturn = user;

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

return createSuccessAuthentication(principalToReturn, authentication, user);
}
  • 首先从 Authentication 提取出登录用户名
  • 然后通过拿着 username 去调用 retrieveUser 方法去获取当前用户对象,这一步会调用我们自己在登录时候的写的 loadUserByUsername 方法,所以这里返回的 user 其实就是你的登录对象
  • 接下来调用 preAuthenticationChecks.check 方法去检验 user 中的各个账户状态属性是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等
  • additionalAuthenticationChecks 方法则是做密码比对的
  • 最后在 postAuthenticationChecks.check 方法中检查密码是否过期
  • 接下来有一个 forcePrincipalAsString 属性,这个是是否强制将 Authentication 中的 principal 属性设置为字符串,这个属性我们一开始在 UsernamePasswordAuthenticationFilter 类中其实就是设置为字符串的(即 username),但是默认情况下,当用户登录成功之后, 这个属性的值就变成当前用户这个对象了。之所以会这样,就是因为 forcePrincipalAsString 默认为 false,不过这块其实不用改,就用 false,这样在后期获取当前用户信息的时候反而方便很多
  • 最后,通过 createSuccessAuthentication 方法构建一个新的 UsernamePasswordAuthenticationToken

整体流程大概如下:

img1

图源:Spring Security认证过程

评论