赞
踩
之前看过很多个版本的 Spring Security 中获取用户信息并进行密码校验,有在相关的Filter中获取的,也有在默认的 DaoAuthenticationProvider 中判断 Authentication
类型进行判断以后直接获取的。
不过,这些方式都不太“正宗”,Spring Security 有自己的一套流程。至于此流程的详细信息讲解,先不急,本篇文章来认识一下其中比较重要的一环:获取用户信息并进行密码验证,即 DaoAuthenticationProvider。
先来认识一下其实现的接口 AuthenticationProvider
,主要用于解析特定 Authentication
。
- /**
- * Indicates a class can process a specific
- * {@link org.springframework.security.core.Authentication} implementation.
- */
其中,有两个接口方法。
首先是 authenticate(Authentication authentication)
方法。
- Authentication authenticate(Authentication authentication)
- throws AuthenticationException;
该方法与 AuthenticationManager 中的 authenticate 声明及功能完全一致,返回包含凭据的完整身份验证对象 authentication
。但是,如果 AuthenticationProvider 不支持给定的 Authentication
的话,该方法可能会返回 null
。在此情况下,下一个支持 authentication
的 AuthenticationProvider 将会被尝试(关于此段逻辑,AuthenticationManager 中会有详细的逻辑代码及说明,后续再详细讲解)。
其次是 supports(Class<?> authentication)
方法。
boolean supports(Class<?> authentication);
如果 AuthenticationProvider
支持给定的 Authentication
的话,会返回 true
。但是,这并不保证 AuthenticationProvider
能够对给定的 Authentication
进行身份认证,它只是表明它可以支持对其进行更深入的评估,AuthenticationProvider
依然可以返回 null
,以指示应尝试另一个 AuthenticationProvider
。
此方法是用以选择一个能够匹配 Authentication
以胜任身份认证工作的 AuthenticationProvider
,交给ProviderManager
来执行。
用于解析 UsernamePasswordAuthenticationToken
以进行身份认证的基础 AuthenticationProvider
。其子类可以重写或使用其 UserDetails
对象。
其实现了 AuthenticationProvider
接口的 authenticate(Authentication authentication)
方法,其认证过程可分为一下几个步骤。
获取用户名。
- String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
- : authentication.getName();
缓存中获取 User。
- boolean cacheWasUsed = true;
- UserDetails user = this.userCache.getUserFromCache(username);
如果缓存中获取的 User 为 null,再调用 retrieveUser(子类实现)方法获取。
- 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;
- }
- }
-
- ......
- }
如果找不到用户,会抛出 UsernameNotFoundException 异常。此时,处理方案有2个。
如果 hideUserNotFoundExceptions
为 true
(默认为 true
),即隐藏用户未找到异常,则会重新抛出凭据/密码错误异常,异常信息为 Spring Security 框架已定义好的提示信息。
如果不隐藏用户未找到异常,则直接抛出 UsernameNotFoundException 异常。
前置身份认证检查。
preAuthenticationChecks.check(user);
默认的前置身份认证检查逻辑如下,即事先校验一下用户的账号是否锁定、是否可用、是否过期。
- private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
- public void check(UserDetails user) {
- if (!user.isAccountNonLocked()) {
- logger.debug("User account is locked");
-
- throw new LockedException(messages.getMessage(
- "AbstractUserDetailsAuthenticationProvider.locked",
- "User account is locked"));
- }
-
- if (!user.isEnabled()) {
- logger.debug("User account is disabled");
-
- throw new DisabledException(messages.getMessage(
- "AbstractUserDetailsAuthenticationProvider.disabled",
- "User is disabled"));
- }
-
- if (!user.isAccountNonExpired()) {
- logger.debug("User account is expired");
-
- throw new AccountExpiredException(messages.getMessage(
- "AbstractUserDetailsAuthenticationProvider.expired",
- "User account has expired"));
- }
- }
- }
额外身份认证校验,对于 UsernamePasswordAuthenticationToken
来说就是凭据/密码校验。
- try {
- preAuthenticationChecks.check(user);
- additionalAuthenticationChecks(user,
- (UsernamePasswordAuthenticationToken) authentication);
- }
- catch (AuthenticationException exception) {
- if (cacheWasUsed) {
- // There was a problem, so try again after checking
- // we're using latest data (i.e. not from the cache)
- cacheWasUsed = false;
- user = retrieveUser(username,
- (UsernamePasswordAuthenticationToken) authentication);
- preAuthenticationChecks.check(user);
- additionalAuthenticationChecks(user,
- (UsernamePasswordAuthenticationToken) authentication);
- }
- else {
- throw exception;
- }
- }
如果认证过程中发生异常,会有如下处理逻辑:
如果当前的用户是从用户缓存中取出的,则使用原有的用户信息再进行一次身份认证,即获取用户信息、前置身份认证检查、额外身份认证检查;
如果不是从用户缓存中取出的,则直接抛出异常。
具体的校验逻辑在下面的 DaoAuthenticationProvider 中会进行详细说明,此处暂且不谈。
后置身份认证检查。
postAuthenticationChecks.check(user);
默认的后置身份认证检查逻辑如下,即检查一下用户的凭据/密码是否过期。
- private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
- public void check(UserDetails user) {
- if (!user.isCredentialsNonExpired()) {
- logger.debug("User account credentials have expired");
-
- throw new CredentialsExpiredException(messages.getMessage(
- "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
- "User credentials have expired"));
- }
- }
- }
将当前用户放入用户缓存,如果当前用户还没有被用户缓存缓存的话。
- if (!cacheWasUsed) {
- this.userCache.putUserInCache(user);
- }
转换 principal 为字符串类型。不过,需要 forcePrincipalAsString
参数为 true
(默认为 false
)。
- Object principalToReturn = user;
-
- if (forcePrincipalAsString) {
- principalToReturn = user.getUsername();
- }
最后,创建身份认证成功的 Authentication
。
return createSuccessAuthentication(principalToReturn, authentication, user);
默认的创建身份认证成功的 Authentication
逻辑如下。
return createSuccessAuthentication(principalToReturn, authentication, user);
即创建了一个新的 UsernamePasswordAuthenticationToken,与未认证的区别就是 principal 变成了检索到的用户详细信息(或者用户名,强制字符串principal)。
当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication
中存储用户提供的原始凭据/密码,而非加盐、加密过的。
AbstractUserDetailsAuthenticationProvider 的主要功能就是这些,剩下的,就是诸如 retrieveUser 、additionalAuthenticationChecks 等需要子类实现的抽象方法了,这就是属于子类 DaoAuthenticationProvider 的部分了。
说到 Authentication
相信都不陌生。最著名的,便是 UsernamePasswordAuthenticationToken
。而 DaoAuthenticationProvider 便是用于解析并认证 UsernamePasswordAuthenticationToken
的这样一个认证服务提供者。
类注释说的也很明白。
- /**
- * An {@link AuthenticationProvider} implementation that retrieves user details from a
- * {@link UserDetailsService}.
- */
其最终目的,就是根据 UsernamePasswordAuthenticationToken
,获取到 username
,然后调用 UserDetailsService
检索用户详细信息。
在其基类 AbstractUserDetailsAuthenticationProvider 中,我们已经讲过,需要子类实现 retrieveUser 、additionalAuthenticationChecks 等抽象方法。那么,我们就来详细看一下吧。
额外的身份认证检查,也即 additionalAuthenticationChecks,密码检查。
- protected void additionalAuthenticationChecks(UserDetails userDetails,
- UsernamePasswordAuthenticationToken authentication)
- throws AuthenticationException {
- 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.matches(presentedPassword, userDetails.getPassword())) {
- logger.debug("Authentication failed: password does not match stored value");
-
- throw new BadCredentialsException(messages.getMessage(
- "AbstractUserDetailsAuthenticationProvider.badCredentials",
- "Bad credentials"));
- }
- }
这里的检查逻辑比较简单,就不再赘述了。
接下来就是用户检索,即 retrieveUser。
- protected final UserDetails retrieveUser(String username,
- UsernamePasswordAuthenticationToken authentication)
- throws AuthenticationException {
- prepareTimingAttackProtection();
- try {
- UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
- if (loadedUser == null) {
- throw new InternalAuthenticationServiceException(
- "UserDetailsService returned null, which is an interface contract violation");
- }
- return loadedUser;
- }
- catch (UsernameNotFoundException ex) {
- mitigateAgainstTimingAttack(authentication);
- throw ex;
- }
- catch (InternalAuthenticationServiceException ex) {
- throw ex;
- }
- catch (Exception ex) {
- throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
- }
- }
正如其类注释所述,需要调用 UserDetailsService
检索用户详细信息,如权限列表、存储密码等,逻辑也比较简单。但是,有两个特殊方法需要注意一下,即检索用户前调用的 prepareTimingAttackProtection()
方法,和抛出 UsernameNotFoundException
异常后调用的 mitigateAgainstTimingAttack(authentication)
方法。
这两个方法都是用于定时攻击保护的。
首先是 prepareTimingAttackProtection()
方法。
- private void prepareTimingAttackProtection() {
- if (this.userNotFoundEncodedPassword == null) {
- this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
- }
- }
该方法从方法命名上理解,是为了准备定时攻击保护用的。其实,就是将 userNotFoundEncodedPassword
,也即用户未找到时的加密密码给准备好,然后使用配置的 passwordEncoder
来加密为密文,默认的用户未找到时的明文密码为 userNotFoundPassword。
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
其次是 mitigateAgainstTimingAttack(authentication)
方法。
- private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
- if (authentication.getCredentials() != null) {
- String presentedPassword = authentication.getCredentials().toString();
- this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
- }
- }
逻辑也不复杂,就是在用户提供的凭据/密码不为空时,使用配置的 passwordEncoder
来验证两者是否相等。看到这里也许会有疑惑,这两者相等的概率太小了吧?一个是用户随便填的,一个是系统配置的,且无论相同与否,都不影响下面 UsernameNotFoundException
异常的抛出,除非我们在自定义的 passwordEncoder
中抛出另外一个非 UsernameNotFoundException
异常。
这里我们看一下方法命名中的 mitigate,意味缓解、减轻、缓和。
什么意思呢?我们需要从整体上想象一下。
用户登录时,调用 AuthenticationProvider
的 authenticate
方法,然后,先从用户缓存中获取用户,如果取不到,再调用 retrieveUser
方法检索用户。最后,再调用 additionalAuthenticationChecks
方法进行密码检查。既然定性为有目的的定时攻击,那么在发生此种情况后,就没有必要继续验证密码了,进一步也没有必要进行后续的身份认证处理了。
至于是否在自定义的 passwordEncoder
中抛出另外一个非 UsernameNotFoundException
异常,还是存疑的,感觉实在没有必要,这里倾向于直接返回密码检查结果,至于 UsernameNotFoundException
异常,在基类调用retrieveUser
方法是已有相关异常捕获逻辑,且可以自行配置是否隐藏该异常类型,所以,感觉没有必要。
这里也确实猜不透设计团队的意图,存个疑吧。有了解此番意图的,可联系作者说明,不胜感激!
另外,DaoAuthenticationProvider 还重写了基类的 createSuccessAuthentication 方法。
- @Override
- protected Authentication createSuccessAuthentication(Object principal,
- Authentication authentication, UserDetails user) {
- boolean upgradeEncoding = this.userDetailsPasswordService != null
- && this.passwordEncoder.upgradeEncoding(user.getPassword());
- if (upgradeEncoding) {
- String presentedPassword = authentication.getCredentials().toString();
- String newPassword = this.passwordEncoder.encode(presentedPassword);
- user = this.userDetailsPasswordService.updatePassword(user, newPassword);
- }
- return super.createSuccessAuthentication(principal, authentication, user);
- }
还记得我们在介绍基类的 createSuccessAuthentication 方法时所说的逻辑吗?
当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication 中存储用户提供的原始凭据/密码,而非加盐、加密过的。
该方法便是对此段逻辑描述最好的呈现了。
至于 userDetailsPasswordService
,其实也很简单,就是更新一下 User 中的密码,仅此而已。
- public UserDetails updatePassword(UserDetails user, String newPassword) {
- String username = user.getUsername();
- MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
- mutableUser.setPassword(newPassword);
- return mutableUser;
- }
还记得前一篇文章中自定义的适用于CA登录认证的 CertificateAuthorityAuthenticationToken 和 CertificateAuthorityAuthenticationFilter 吗?
相对应的,这里我们就自定义个适用于CA登录认证的 CertificateAuthorityAuthenticationProvider。
- public class CertificateAuthorityDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
-
- private UserDetailsService userDetailsService;
-
- public CertificateAuthorityDaoAuthenticationProvider() {
- }
-
- @Override
- public boolean supports(Class<?> authentication) {
- return (CertificateAuthorityAuthenticationToken.class.isAssignableFrom(authentication));
- }
-
- @SuppressWarnings("deprecation")
- protected void additionalAuthenticationChecks(UserDetails userDetails,
- UsernamePasswordAuthenticationToken authentication)
- throws AuthenticationException {
- // do nothing.
- }
-
- ......
-
- }
由于CA登录时,其CA KEY的密码是有前端配合控制器校验的,况且其密码也不在业务数据库中存储,不需要Spring Security框架处理,所以,无需进行额外的身份认证检查。
另外,需要修改一下 supports
方法,仅支持 CertificateAuthorityAuthenticationToken
类型的 Authentication
哦。
其它详细源码,请参考文末源码链接,可自行下载后阅读。
github
https://github.com/liuminglei/SpringSecurityLearning/tree/master/26
gitee
https://gitee.com/xbd521/SpringSecurityLearning/tree/master/26
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。