写在前面

在前面学习前后端分离模式下的回调和注销时,提到了密码擦除,当时没有对SpringSecurity的登录流程进行梳理,那么本篇就来详细学习登录流程。

场景描述

现在有一个场景,用户在服务端安全管理选择了SpringSecurity,那么用户登录成功后,SpringSecurity会将用户信息保存在Session中,但是具体保存的位置,就目前而言开发者是不知道的,但是现在就是想知道这个信息的保存位置,以便当用户在前端修改了自己的信息,在不重新登录的情况下,开发者如何获取到用户的最新信息?这个场景在实际工作中是很常见的。

Authentication对象

如果你之前使用过Shiro框架,那么就知道在Shiro框架中与用户认证相关的信息都在AuthenticationToken接口中,查看一下这个接口的源码:

1
2
3
4
5
public interface AuthenticationToken extends Serializable {
Object getPrincipal();

Object getCredentials();
}

它只有两个方法,一个获取用户名,一个获取用户密码。而我们常用于认证的UsernamePasswordToken类就是实现了这个接口,这个UsernamePasswordToken类中包含username、password、rememberMe和host这四个属性以及对应的getter和setetr方法。

其实SpringSecurity框架也存在类似的代码,所不同的是SpringSecurity框架中与认证相关的是Authentication接口,查看一下这个接口的源码:

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;
}

乍一看是不是有一种似曾相似的感觉?是的,前一篇《Spring Data JPA操作数据库》一文中在自定义UserDetails时,实现了UserDetails接口,查看一下这个UserDetails接口的源码,如下所示:

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

String getPassword();

String getUsername();

boolean isAccountNonExpired();

boolean isAccountNonLocked();

boolean isCredentialsNonExpired();

boolean isEnabled();
}

是不是非常相似,第一个都是获取角色信息,之后是获取用户名和密码等信息。

回到Authentication接口中来,前面说过这个接口只有六个方法,分别用于获取角色信息、用户名、用户信息、密码、是否验证、设置验证等,因此开发者可以在需要使用的地方注入Authentication对象,进而获取当前登录用户的信息。顺便查看一下这个接口的实现类:

可以看到它存在6个实现类,如下所示:AbstractAuthenticationTokenAnonymousAuthenticationTokenJaasAuthenticationTokenPreAuthenticatedAuthenticationTokenRememberMeAuthenticationTokenTestingAuthenticationTokenUsernamePasswordAuthenticationToken等,同样我们常用于认证的就是UsernamePasswordAuthenticationToken类,查看一下这个UsernamePasswordAuthenticationToken类的源码,如下所示:

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
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 540L;
private final Object principal;
private Object credentials;

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}

public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}

public Object getCredentials() {
return this.credentials;
}

public Object getPrincipal() {
return this.principal;
}

public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}

public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}

可以看到这个类中有两个有参的构造方法、两个属性及其对应的getter/setter方法,这么看似鸡肋的类,其实主要功能还得看它继承的AbstractAuthenticationToken类。这两个属性principal和credentials其实就是用户名和密码,因此开发者是完全可以从这个类中获取用户名和密码。但是到现在为止,我们还是不知道登录信息是如何保存到这两个对象中的,仅仅知道可以从这个类中获取登录信息,为此我们需要对登录流程进行全面梳理。

登录流程

在Shiro框架中认证和授权的校验都是在一系列的过滤器链中完成的,如下所示:

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
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
System.out.println("******shiroFilter******");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置拦截器
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
//配置不会被拦截的链接,此时会按照顺序进行判断
//anon是匿名访问,authc是通过认证后才能访问
filterChainDefinitionMap.put("/static/**","anon");
//配置退出过滤器,具体已经由Shiro实现了
filterChainDefinitionMap.put("/logout","logout");
//配置过滤链,它会从上到下顺序执行,因此一般将/**放在最下面
filterChainDefinitionMap.put("/**","authc");


//如果不设置,则默认会自动寻找Web工程根目录下的login.html页面
shiroFilterFactoryBean.setLoginUrl("/login");
//设置登录成功后需要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");

//设置未授权页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}

与此类似的,SpringSecurity的认证与授权也是在一系列的过滤器链中完成的,其中与认证相关的过滤器为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
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
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;

public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}

public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}

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);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
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);
}

protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}

public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}

public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}

public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

public final String getUsernameParameter() {
return this.usernameParameter;
}

public final String getPasswordParameter() {
return this.passwordParameter;
}
}

其实这个类在前面学习“自定义登录参数名称”的时候就已经简单学习过了,接下来对登录流程进行分析:
(1)调用attemptAuthentication方法来进行尝试验证;
(2)之后(1)中通过obtainUsernameobtainPassword方法获取请求中的用户名和密码,其底层使用了request.getParameter()方法。也就是说这两个方法都是从表单控件中获取登录信息,即需要通过key/value键值对来传递参数。请注意不能使用JOSN来传递参数,开发者如果想通过JOSN来传递参数,那么就需要修改此处逻辑。
(3)接着(1)传入username和password参数来构造一个UsernamePasswordAuthenticationToken对象,查看一下这个对象的构造方法:

1
2
3
4
5
6
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}

也就是说这里的username其实就对应于UsernamePasswordAuthenticationToken对象中的principal属性,password对应于UsernamePasswordAuthenticationToken对象中的credentials属性。
(4)接下来(1)中调用setDetails方法,将request和UsernamePasswordAuthenticationToken对象作为参数传入进去,但是细心的你可能发现这个UsernamePasswordAuthenticationToken对象是没有details这个属性的,它没有这个属性,那就查看一下它的父类AbstractAuthenticationToken,可以发现它的父类是有的:

1
2
3
4
5
6
7
8
9
private Object details;

public Object getDetails() {
return this.details;
}

public void setDetails(Object details) {
this.details = details;
}

现在问题来了,这个details对象中保存的是什么呢?查看一下UsernamePasswordAuthenticationFilter类中setDetails方法的源码,如下所示:

1
2
3
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}

可以发现此处调用了authenticationDetailsSourcebuildDetails()方法,查看一下源码:

1
2
3
public interface AuthenticationDetailsSource<C, T> {
T buildDetails(C var1);
}

也就是说最后得到的其实是一个WebAuthenticationDetails对象,这个对象主要描述了请求的remoteAddress和sessionId信息。关于这一部分内容,会在后面《获取登录额外信息》一文中进行详细介绍。
(5)最后(1)中调用authenticate()方法去做认证。

可以看到这里所说的登录流程,其实就是attemptAuthentication方法的执行过程。

认证过程

说完了前面的登录流程,再来看后面的认证过程。我们从attemptAuthentication方法的最后一步来进行分析。它首先需要获取到一个AuthenticationManager对象, 这个AuthenticationManager接口有5个实现类,这里使用的是ProviderManager类。之后调用ProviderManager类的authenticate()方法,因此接下来查看这个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
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;
int currentPosition = 0;
int size = this.providers.size();
Iterator var9 = this.getProviders().iterator();

while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
if (provider.supports(toTest)) {
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}

try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}

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

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

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

return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}

if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}

throw lastException;
}
}

可以看到这个方法中包含的代码非常多,因为几乎所有关于认证的逻辑都在这个方法中。接下来开始分析这个方法:
(1)调用authentication.getClass()方法来获取当前的Class信息:

1
Class<? extends Authentication> toTest = authentication.getClass();

(2)通过getProviders().iterator()方法来得到一个迭代器对象,其类型都是AuthenticationProvider

1
2
3
4
5
6
Iterator var9 = this.getProviders().iterator();

while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
......
}

(3)判断当前provider对象是否支持(1)中authentication对象。如果支持,则调用provider的 authenticate()方法开始认证校验,校验完成后会返回一个新的 Authentication对象:

1
2
3
4
5
6
7
8
9
if (provider.supports(toTest)) {
......
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
}

(4)当(3)中返回的对象不为空,则调用copyDetails()方法将旧AbstractAuthenticationToken对象中的details属性复制到新的AbstractAuthenticationToken对象中:

1
2
3
4
5
6
private void copyDetails(Authentication source, Authentication dest) {
if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest;
token.setDetails(source.getDetails());
}
}

(5)当(3)中返回的对象为空,且AuthenticationManager对象不为空,则调用AuthenticationManager对象的authenticate()方法来进行认证校验。
(6)之后调用eraseCredentials方法将凭证信息擦除掉,这里的凭证信息就是用户密码:

1
2
3
4
5
6
7
8
9
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}

它首先判断(3)中的返回对象不为空,且对象是CredentialsContainer的实例,为什么需要判断?那是因为只有这个CredentialsContainer接口中才存在擦除凭证信息的eraseCredentials()方法:

1
2
3
public interface CredentialsContainer {
void eraseCredentials();
}

但是这是一个接口,那么接下看一下它的实现类,可以发现一个比较熟悉的身影UsernamePasswordAuthenticationToken,这里面的eraseCredentials()方法进行了实现:

1
2
3
4
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}

可以看到它擦除凭证的逻辑非常简单,就是将UsernamePasswordAuthenticationToken对象中的credentials属性设置为null,其实就是密码设置为空。
(7)通过调用MessageSourceAccessor对象的publishAuthenticationSuccess()方法将(3)中成功返回的对象广播出去:

1
2
3
4
5
6
7
8
9
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}

以上就是认证的整个流程,第一遍看的时候非常容易晕,这是正常现象,第一次看源码很正常,看多了就习惯了。

这里再捋一下上面的逻辑,首先得到当前Authentication对象的Class信息,接着通过getProviders().iterator()方法得到一个迭代器对象,之后遍历这个迭代器对象,请注意不是所有的AuthenticationProvider对象都支持Authentication,如AnonymousAuthenticationProvider就不支持,因此它调用AuthenticationManager接口对象(代码中的parent)的authenticate()方法进行认证校验。而这个AuthenticationManager接口对象的实现类是ProviderManager,因此又回到了此处的authenticate方法中。不过此时的provider就变成了DaoAuthenticationProvider,这个provider是支持Authentication的(如UsernamePasswordAuthenticationToken),因此会进入到DaoAuthenticationProvider类的authenticate方法中执行,但是笔者并没有在DaoAuthenticationProvider类中发现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
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = this.determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;

try {
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw var6;
}

throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}

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

try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}

cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}

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

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

return this.createSuccessAuthentication(principalToReturn, authentication, user);
}

仔细查看上述代码的逻辑:
(1)判断authentication对象是否是UsernamePasswordAuthenticationToken类型的实例,如果是的话则从Authentication中获取当前登录用户的用户名;
(2)从缓存中通过用户名来获取用户信息,如果获取不到用户信息则说明缓存未启用;反之得到用户信息;
(3)调用retrieveUser方法,将得到的用户名和之前的UsernamePasswordAuthenticationToken对象作为参数传入,查看一下这个retrieveUser方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();

try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}

可以看到它首先调用getUserDetailsService()方法得到一个UserDetailsService对象,之后调用它的loadUserByUsername()方法来获取UserDetails对象,注意到了么,这个就是之前我们自定义的UserDetailService类中的loadUserByUsername()方法,也就是说此处返回的UserDetails对象其实就是登录对象User:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findUserByUsername(username);
if(user==null){
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}

(4)调用preAuthenticationChecks.check(user)方法来检查User对象的各个属性是否正常。查看一下这个check方法,发现它存在于UserDetailsChecker接口中:

1
2
3
public interface UserDetailsChecker {
void check(UserDetails var1);
}

为了弄清楚这个check方法的执行逻辑,这里需要查看UserDetailsChecker接口的实现类AccountStatusUserDetailsChecker,在这个实现类中就有check方法的具体实现逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
this.logger.debug("Failed to authenticate since user account is locked");
throw new LockedException(this.messages.getMessage("AccountStatusUserDetailsChecker.locked", "User account is locked"));
} else if (!user.isEnabled()) {
this.logger.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(this.messages.getMessage("AccountStatusUserDetailsChecker.disabled", "User is disabled"));
} else if (!user.isAccountNonExpired()) {
this.logger.debug("Failed to authenticate since user account is expired");
throw new AccountExpiredException(this.messages.getMessage("AccountStatusUserDetailsChecker.expired", "User account has expired"));
} else if (!user.isCredentialsNonExpired()) {
this.logger.debug("Failed to authenticate since user account credentials have expired");
throw new CredentialsExpiredException(this.messages.getMessage("AccountStatusUserDetailsChecker.credentialsExpired", "User credentials have expired"));
}
}

可以看到这里主要对用户的账户是否未锁住、是否可用、是否未过期、密码是否过期等进行判断,满足其中任意一个都会抛出异常。
(5)接着调用additionalAuthenticationChecks()方法来对用户密码进行判断,查看这个方法的源码,如下所示:

1
protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;

这是一个抽象方法,得找一个这个抽象类的具体实现才行,又回到了DaoAuthenticationProvider这个类,可以发现这个类中的additionalAuthenticationChecks方法就提供了具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}

在这个方法中首先会判断用户密码是否为空,如果为空则抛出异常;不为空则得到当前登录用户的密码,之后调用passwordEncoder.matches()方法将之前获取到的密码与数据库中存储的密码是否一致,不一致则抛出异常。以BCryptPasswordEncoder类中的matches方法为例,查看一下密码的匹配逻辑,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}

其中rawPassword是获取到的用户密码,而encodedPassword则是数据库中查询到的密码,密码匹配逻辑其实较为简单。

(6)回到AbstractUserDetailsAuthenticationProvider类的authenticate方法中,密码验证通过后,接下来调用postAuthenticationChecks.check(user)方法来检查密码是否过期。

(7)之后判断forcePrincipalAsString属性是否为true,该属性是强制将Principal(用户名)转换为String(字符串)对象。其实在UsernamePasswordAuthenticationFilter类中就已经将principal(用户名)设置为字符串,但是在默认情况下,当用户登录成功后,这个属性的值就成了当前用户,也就是UserDetails对象。需要说明的是,这个forcePrincipalAsString属性默认值为false,其实开发者不需要管这个参数,因为没转成字符串,反而更利于后续获取用户信息:

1
2
3
4
5
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);

(8)最后调用createSuccessAuthentication()方法来构造一个Authentication对象,准确来说是UsernamePasswordAuthenticationToken对象:

1
2
3
4
5
6
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}

以上就是用户的登录流程和认证过程,但是我们依旧不知道登录的用户信息是在哪个环节被保存的,只有搞清楚这个才算真的搞清楚了用户登录。

保存用户信息

我们重新捋一下思路,想想上面的登录是从哪来开始执行的,或者说是触发的?

回到UsernamePasswordAuthenticationFilter类,这个类非常重要,登录流程就是从这开始的,查看一下它的父类AbstractAuthenticationProcessingFilter,请注意这个类以后会经常使用,当开发者需要在SpringSecurity中自定义一个登录验证码或者将登录参数修改为JSON的时候,此时都需要自定义自己的Filter类,并继承这个AbstractAuthenticationProcessingFilter类。

如果你之前使用过Servlet,那么肯定对Filter不陌生,在需要对某些资源进行过滤拦截并进行处理,之后返回处理过的内容,就需要自定义Filter类并实现Filter接口,可以点击 这里 进行了解。这个接口中有一个doFilter方法,当拦截到需要执行的请求时,doFilter方法就会执行,开发者可以在这个方法里面书写对请求和响应的预处理逻辑。

AbstractAuthenticationProcessingFilter这个抽象类中,我们也发现它也存在名为doFilter的方法,源码如下所示:

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
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
try {
Authentication authenticationResult = this.attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}

this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}

this.successfulAuthentication(request, response, chain, authenticationResult);
} catch (InternalAuthenticationServiceException var5) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
this.unsuccessfulAuthentication(request, response, var5);
} catch (AuthenticationException var6) {
this.unsuccessfulAuthentication(request, response, var6);
}

}
}

仔细阅读上述方法的源码,可以发现首先调用requiresAuthentication(request, response)方法来判断是否需要验证,如果不需要就直接执行chain.doFilter()方法;如果需要验证,那么会调用attemptAuthentication()方法,看到没有,原来是在此处调用了UsernamePasswordAuthenticationFilter#attemptAuthentication()方法;且当用户登录成功时,就调用successfulAuthentication()方法,失败则调用unsuccessfulAuthentication()方法。

接下来看一下登录成功时所调用的successfulAuthentication()方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}

this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}

this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

注意这里面第一行代码SecurityContextHolder.getContext().setAuthentication(authResult);,它的作用是获取上下文,并将之前的验证结果赋值给Authentication属性,原来成功的登录信息都会被保存在这里,因此当我们需要获取用户的登录信息时,可以直接从SecurityContextHolder.getContext();中获取,这其实是一个SecurityContext对象。当然如果想修改用户信息,也只需修改这个SecurityContext对象。

回到上面的successfulAuthentication方法中,继续往下读源码,之后会调用rememberMeServices.loginSuccess()方法,将登录信息写入“记住我”中,这个是当用户使用了“记住我”这个功能才有效。接着判断是否存在事件广播者,如果存在则将用户登录成功的信息发布出去。如果不存在则调用successHandler.onAuthenticationSuccess()方法,而这个方法就是我们之前在MySecurityConfig类中配置登录成功时的回调方法,原来那个方法是在这里被触发的:

再来看一下登录失败时所调用的unsuccessfulAuthentication()方法的源码:

1
2
3
4
5
6
7
8
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}

同样注意这里面的第一行代码SecurityContextHolder.clearContext();,它的作用是获取上下文,并将上下文内容清空。之后调用rememberMeServices.loginFail()方法来说明“记住我”也是登录失败的。最后调用failureHandler.onAuthenticationFailure()方法,而这个方法同样也是之前我们在MySecurityConfig类中配置登录失败时的回调方法,原来那个方法是在这里被触发的:

看到这里我们就对登录流程和认证过程有了清晰的认识,也知道了原来数据保存在SecurityContextHolder.getContext();中,当开发者需要获取和修改登录信息时,只需对该SecurityContext对象进行操作即可。

ok,那么本篇关于登录流程的详细分析就到此为止,后续学习其他内容。