当前位置:   article > 正文

史上最简单的Spring Security教程(二十六):DaoAuthenticationProvider详解

daoauthenticationprovider

之前看过很多个版本的 Spring Security 中获取用户信息并进行密码校验,有在相关的Filter中获取的,也有在默认的 DaoAuthenticationProvider 中判断 Authentication 类型进行判断以后直接获取的。

不过,这些方式都不太“正宗”,Spring Security 有自己的一套流程。至于此流程的详细信息讲解,先不急,本篇文章来认识一下其中比较重要的一环:获取用户信息并进行密码验证,即 DaoAuthenticationProvider

 

AuthenticationProvider

 

先来认识一下其实现的接口 AuthenticationProvider,主要用于解析特定 Authentication 。

  1. /**
  2. * Indicates a class can process a specific
  3. * {@link org.springframework.security.core.Authentication} implementation.
  4.  */

其中,有两个接口方法。

首先是 authenticate(Authentication authentication) 方法。

  1. Authentication authenticate(Authentication authentication)
  2.     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 来执行。

 

AbstractUserDetailsAuthenticationProvider

 

用于解析 UsernamePasswordAuthenticationToken 以进行身份认证的基础 AuthenticationProvider。其子类可以重写或使用其 UserDetails 对象。

其实现了 AuthenticationProvider 接口的 authenticate(Authentication authentication) 方法,其认证过程可分为一下几个步骤。

获取用户名。

  1. String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
  2. : authentication.getName();

缓存中获取 User。

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

如果缓存中获取的 User 为 null,再调用 retrieveUser(子类实现)方法获取。

  1. if (user == null) {
  2. cacheWasUsed = false;
  3. try {
  4. user = retrieveUser(username,
  5. (UsernamePasswordAuthenticationToken) authentication);
  6. }
  7. catch (UsernameNotFoundException notFound) {
  8. logger.debug("User '" + username + "' not found");
  9. if (hideUserNotFoundExceptions) {
  10. throw new BadCredentialsException(messages.getMessage(
  11. "AbstractUserDetailsAuthenticationProvider.badCredentials",
  12. "Bad credentials"));
  13. }
  14. else {
  15. throw notFound;
  16. }
  17. }
  18. ......
  19. }

如果找不到用户,会抛出 UsernameNotFoundException 异常。此时,处理方案有2个。

  • 如果 hideUserNotFoundExceptions 为 true默认为 true),即隐藏用户未找到异常,则会重新抛出凭据/密码错误异常,异常信息为 Spring Security 框架已定义好的提示信息。

  • 如果不隐藏用户未找到异常,则直接抛出 UsernameNotFoundException 异常。

前置身份认证检查。

preAuthenticationChecks.check(user);

默认的前置身份认证检查逻辑如下,即事先校验一下用户的账号是否锁定、是否可用、是否过期

  1. private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
  2. public void check(UserDetails user) {
  3. if (!user.isAccountNonLocked()) {
  4. logger.debug("User account is locked");
  5. throw new LockedException(messages.getMessage(
  6. "AbstractUserDetailsAuthenticationProvider.locked",
  7. "User account is locked"));
  8. }
  9. if (!user.isEnabled()) {
  10. logger.debug("User account is disabled");
  11. throw new DisabledException(messages.getMessage(
  12. "AbstractUserDetailsAuthenticationProvider.disabled",
  13. "User is disabled"));
  14. }
  15. if (!user.isAccountNonExpired()) {
  16. logger.debug("User account is expired");
  17. throw new AccountExpiredException(messages.getMessage(
  18. "AbstractUserDetailsAuthenticationProvider.expired",
  19. "User account has expired"));
  20. }
  21. }
  22. }

额外身份认证校验,对于 UsernamePasswordAuthenticationToken 来说就是凭据/密码校验。

  1. try {
  2. preAuthenticationChecks.check(user);
  3. additionalAuthenticationChecks(user,
  4. (UsernamePasswordAuthenticationToken) authentication);
  5. }
  6. catch (AuthenticationException exception) {
  7. if (cacheWasUsed) {
  8. // There was a problem, so try again after checking
  9. // we're using latest data (i.e. not from the cache)
  10. cacheWasUsed = false;
  11. user = retrieveUser(username,
  12. (UsernamePasswordAuthenticationToken) authentication);
  13. preAuthenticationChecks.check(user);
  14. additionalAuthenticationChecks(user,
  15. (UsernamePasswordAuthenticationToken) authentication);
  16. }
  17. else {
  18. throw exception;
  19. }
  20. }

如果认证过程中发生异常,会有如下处理逻辑:

  1. 如果当前的用户是从用户缓存中取出的,则使用原有的用户信息再进行一次身份认证,即获取用户信息、前置身份认证检查、额外身份认证检查;

  2. 如果不是从用户缓存中取出的,则直接抛出异常。

具体的校验逻辑在下面的 DaoAuthenticationProvider 中会进行详细说明,此处暂且不谈。

后置身份认证检查。

postAuthenticationChecks.check(user);

默认的后置身份认证检查逻辑如下,即检查一下用户的凭据/密码是否过期。

  1. private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
  2. public void check(UserDetails user) {
  3. if (!user.isCredentialsNonExpired()) {
  4. logger.debug("User account credentials have expired");
  5. throw new CredentialsExpiredException(messages.getMessage(
  6. "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
  7. "User credentials have expired"));
  8. }
  9. }
  10. }

将当前用户放入用户缓存,如果当前用户还没有被用户缓存缓存的话。

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

转换 principal 为字符串类型。不过,需要 forcePrincipalAsString 参数为 true(默认为 false)。

  1. Object principalToReturn = user;
  2. if (forcePrincipalAsString) {
  3. principalToReturn = user.getUsername();
  4. }

最后,创建身份认证成功的 Authentication

return createSuccessAuthentication(principalToReturn, authentication, user);

默认的创建身份认证成功的 Authentication 逻辑如下。

return createSuccessAuthentication(principalToReturn, authentication, user);

即创建了一个新的 UsernamePasswordAuthenticationToken,与未认证的区别就是 principal 变成了检索到的用户详细信息(或者用户名,强制字符串principal

当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication 中存储用户提供的原始凭据/密码,而非加盐、加密过的。

AbstractUserDetailsAuthenticationProvider 的主要功能就是这些,剩下的,就是诸如 retrieveUser 、additionalAuthenticationChecks 等需要子类实现的抽象方法了,这就是属于子类 DaoAuthenticationProvider 的部分了。

 

DaoAuthenticationProvider

 

说到 Authentication 相信都不陌生。最著名的,便是 UsernamePasswordAuthenticationToken。而 DaoAuthenticationProvider 便是用于解析并认证 UsernamePasswordAuthenticationToken 的这样一个认证服务提供者。

类注释说的也很明白。

  1. /**
  2. * An {@link AuthenticationProvider} implementation that retrieves user details from a
  3. * {@link UserDetailsService}.
  4. */

其最终目的,就是根据 UsernamePasswordAuthenticationToken,获取到 username,然后调用 UserDetailsService 检索用户详细信息。

在其基类 AbstractUserDetailsAuthenticationProvider 中,我们已经讲过,需要子类实现 retrieveUser 、additionalAuthenticationChecks 等抽象方法那么,我们就来详细看一下吧

额外的身份认证检查,也即 additionalAuthenticationChecks,密码检查。

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

这里的检查逻辑比较简单,就不再赘述了。

接下来就是用户检索,即 retrieveUser

  1. protected final UserDetails retrieveUser(String username,
  2. UsernamePasswordAuthenticationToken authentication)
  3. throws AuthenticationException {
  4. prepareTimingAttackProtection();
  5. try {
  6. UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
  7. if (loadedUser == null) {
  8. throw new InternalAuthenticationServiceException(
  9. "UserDetailsService returned null, which is an interface contract violation");
  10. }
  11. return loadedUser;
  12. }
  13. catch (UsernameNotFoundException ex) {
  14. mitigateAgainstTimingAttack(authentication);
  15. throw ex;
  16. }
  17. catch (InternalAuthenticationServiceException ex) {
  18. throw ex;
  19. }
  20. catch (Exception ex) {
  21. throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
  22. }
  23. }

正如其类注释所述,需要调用 UserDetailsService 检索用户详细信息,如权限列表、存储密码等,逻辑也比较简单。但是,有两个特殊方法需要注意一下,即检索用户前调用的 prepareTimingAttackProtection() 方法,和抛出 UsernameNotFoundException 异常后调用的 mitigateAgainstTimingAttack(authentication) 方法。

这两个方法都是用于定时攻击保护的。

首先是 prepareTimingAttackProtection() 方法。

  1. private void prepareTimingAttackProtection() {
  2. if (this.userNotFoundEncodedPassword == null) {
  3. this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
  4. }
  5. }

该方法从方法命名上理解,是为了准备定时攻击保护用的。其实,就是将 userNotFoundEncodedPassword,也即用户未找到时的加密密码给准备好,然后使用配置的 passwordEncoder 来加密为密文,默认的用户未找到时的明文密码为 userNotFoundPassword。

private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

其次是 mitigateAgainstTimingAttack(authentication) 方法。

  1. private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
  2. if (authentication.getCredentials() != null) {
  3. String presentedPassword = authentication.getCredentials().toString();
  4. this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
  5. }
  6. }

逻辑也不复杂,就是在用户提供的凭据/密码不为空时,使用配置的 passwordEncoder 来验证两者是否相等。看到这里也许会有疑惑,这两者相等的概率太小了吧?一个是用户随便填的,一个是系统配置的,且无论相同与否,都不影响下面 UsernameNotFoundException 异常的抛出,除非我们在自定义的 passwordEncoder 中抛出另外一个非 UsernameNotFoundException 异常。

这里我们看一下方法命名中的 mitigate,意味缓解、减轻、缓和。

什么意思呢?我们需要从整体上想象一下。

用户登录时,调用 AuthenticationProvider 的 authenticate 方法,然后,先从用户缓存中获取用户,如果取不到,再调用 retrieveUser 方法检索用户。最后,再调用 additionalAuthenticationChecks 方法进行密码检查。既然定性为有目的的定时攻击,那么在发生此种情况后,就没有必要继续验证密码了,进一步也没有必要进行后续的身份认证处理了。

至于是否在自定义的 passwordEncoder 中抛出另外一个非 UsernameNotFoundException 异常,还是存疑的,感觉实在没有必要,这里倾向于直接返回密码检查结果,至于 UsernameNotFoundException 异常,在基类调用retrieveUser 方法是已有相关异常捕获逻辑,且可以自行配置是否隐藏该异常类型,所以,感觉没有必要。

这里也确实猜不透设计团队的意图,存个疑吧。有了解此番意图的,可联系作者说明,不胜感激!

另外,DaoAuthenticationProvider 还重写了基类的 createSuccessAuthentication 方法。

  1. @Override
  2. protected Authentication createSuccessAuthentication(Object principal,
  3. Authentication authentication, UserDetails user) {
  4. boolean upgradeEncoding = this.userDetailsPasswordService != null
  5. && this.passwordEncoder.upgradeEncoding(user.getPassword());
  6. if (upgradeEncoding) {
  7. String presentedPassword = authentication.getCredentials().toString();
  8. String newPassword = this.passwordEncoder.encode(presentedPassword);
  9. user = this.userDetailsPasswordService.updatePassword(user, newPassword);
  10. }
  11. return super.createSuccessAuthentication(principal, authentication, user);
  12. }

还记得我们在介绍基类的 createSuccessAuthentication 方法时所说的逻辑吗?

当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication 中存储用户提供的原始凭据/密码,而非加盐、加密过的。

该方法便是对此段逻辑描述最好的呈现了。

至于 userDetailsPasswordService,其实也很简单,就是更新一下 User 中的密码,仅此而已。

  1. public UserDetails updatePassword(UserDetails user, String newPassword) {
  2. String username = user.getUsername();
  3. MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
  4. mutableUser.setPassword(newPassword);
  5. return mutableUser;
  6. }

 

自定义Provider

 

还记得前一篇文章中自定义的适用于CA登录认证的 CertificateAuthorityAuthenticationToken 和 CertificateAuthorityAuthenticationFilter 吗?

相对应的,这里我们就自定义个适用于CA登录认证的 CertificateAuthorityAuthenticationProvider。

  1. public class CertificateAuthorityDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
  2. private UserDetailsService userDetailsService;
  3. public CertificateAuthorityDaoAuthenticationProvider() {
  4. }
  5. @Override
  6. public boolean supports(Class<?> authentication) {
  7. return (CertificateAuthorityAuthenticationToken.class.isAssignableFrom(authentication));
  8. }
  9. @SuppressWarnings("deprecation")
  10. protected void additionalAuthenticationChecks(UserDetails userDetails,
  11. UsernamePasswordAuthenticationToken authentication)
  12. throws AuthenticationException {
  13. // do nothing.
  14. }
  15. ......
  16. }

由于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
 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/534042
推荐阅读
相关标签
  

闽ICP备14008679号