likes
comments
collection
share

[SpringSecurity5.6.2源码分析十五]:ProviderManager

作者站长头像
站长
· 阅读数 59

前言

  • ProviderManager是AuthenticationManager最重要的一个实现类,是整个认证逻辑的入口类

[SpringSecurity5.6.2源码分析十五]:ProviderManager

1. authenticate(...)

  • authenticate是ProviderManager的核心方法,也是入口方法
@Override
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();
   for (AuthenticationProvider provider : getProviders()) {
      //判断当前认证提供者是否支持这个认证对象
      if (!provider.supports(toTest)) {
         continue;
      }
      if (logger.isTraceEnabled()) {
         logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
               provider.getClass().getSimpleName(), ++currentPosition, size));
      }
      try {
         //进行认证
         result = provider.authenticate(authentication);
         if (result != null) {
            //复制详细信息到新的认证对象中
            copyDetails(authentication, result);
            break;
         }
      }
      catch (AccountStatusException | InternalAuthenticationServiceException ex) {
         prepareException(ex, authentication);
         //如果认证失败是由于无效的帐户状态导致的,则抛出异常,避免轮询执行其他认证提供者
         throw ex;
      }
      catch (AuthenticationException ex) {
         lastException = ex;
      }
   }
   //到这就说明局部管理器无法认证,尝试调用父类(全局认证管理器)
   if (result == null && this.parent != null) {
      try {
         //进行认证
         parentResult = this.parent.authenticate(authentication);
         //两个都有了
         result = parentResult;
      }
      catch (ProviderNotFoundException ex) {
         // ignore as we will throw below if no other exception occurred prior to
         // calling parent and the parent
         // may throw ProviderNotFound even though a provider in the child already
         // handled the request
      }
      catch (AuthenticationException ex) {
         parentException = ex;
         lastException = ex;
      }
   }
   //如果认证成功
   if (result != null) {
      //是否在认证成功后清除敏感数据
      if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
         //比如说清除密码
         ((CredentialsContainer) result).eraseCredentials();
      }

      //如果是局部自己就认证成功的,发布一个认证成功事件
      if (parentResult == null) {
         this.eventPublisher.publishAuthenticationSuccess(result);
      }

      return result;
   }

   //如果中途抛出了异常
   if (lastException == null) {
      //统一包装成一个异常
      lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
            new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
   }
   //如果只是局部抛出了,就发布一个认证失败异常
   if (parentException == null) {
      prepareException(lastException, authentication);
   }
   throw lastException;
}
  • authenticate(...)方法的核心逻辑其实就是一个for循环调用内部的AuthenticationProvider进行认证,如果当前AuthenticationManager中的AuthenticationProvider无法认证,就调用父类(全局认证管理器)进行认证
  • 所以说存在子类和父类两个认证管理器

2. AuthenticationProvider

  • AuthenticationProvider的实现类比较多,现只介绍默认注册的,其他的会随着对应的过滤器进行介绍
public interface AuthenticationProvider {

   /**
    * 开始认证
    */
   Authentication authenticate(Authentication authentication) throws AuthenticationException;

   /**
    * 判断是否支持这种认证对象的认证,通常是比较Class对象
    */
   boolean supports(Class<?> authentication);

}

2.1 AnonymousAuthenticationProvider

  • 由AnonymousConfigurer负责注册,AnonymousConfigurer也是默认注册的配置类
  • 分析源码看出无非是判断传入的AnonymousAuthenticationToken中的key是否是正确的
public class AnonymousAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {

   /**
    * 比较是否是匿名用户过滤器创建的认证对象
    */
   private String key;

   public AnonymousAuthenticationProvider(String key) {
      Assert.hasLength(key, "A Key is required");
      this.key = key;
   }

   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      //判断是否是匿名认证对象
      if (!supports(authentication.getClass())) {
         return null;
      }
      //比较key
      if (this.key.hashCode() != ((AnonymousAuthenticationToken) authentication).getKeyHash()) {
         throw new BadCredentialsException(this.messages.getMessage("AnonymousAuthenticationProvider.incorrectKey",
               "The presented AnonymousAuthenticationToken does not contain the expected key"));
      }
      return authentication;
   }

    @Override
    public boolean supports(Class<?> authentication) {
       return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
    }

}
  • AnonymousAuthenticationToken可以理解为认证对象,是在AnonymousAuthenticationFilter中创建的
  • 下面就是AnonymousAuthenticationFilter的源码
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
   //当前会话没有认证对象的时候,创建一个匿名认证对象
   if (SecurityContextHolder.getContext().getAuthentication() == null) {
      //创建匿名认证对象
      Authentication authentication = createAuthentication((HttpServletRequest) req);
      //创建安全上下文
      SecurityContext context = SecurityContextHolder.createEmptyContext();
      context.setAuthentication(authentication);
      //设置到线程级别的安全上下文策略中
      SecurityContextHolder.setContext(context);
      if (this.logger.isTraceEnabled()) {
         this.logger.trace(LogMessage.of(() -> "Set SecurityContextHolder to "
               + SecurityContextHolder.getContext().getAuthentication()));
      }
      else {
         this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
      }
   }
   else {
      if (this.logger.isTraceEnabled()) {
         this.logger.trace(LogMessage.of(() -> "Did not set SecurityContextHolder since already authenticated "
               + SecurityContextHolder.getContext().getAuthentication()));
      }
   }
   chain.doFilter(req, res);
}

[SpringSecurity5.6.2源码分析十五]:ProviderManager

  • 分析上图和AnonymousAuthenticationFilter的源码就可以得出AnonymousAuthenticationToken是SpringSecurity的一个保底策略
  • 确保我们使用SecurityContextHolder.getContext().getAuthentication()至少有一个对象

2.2 DaoAuthenticationProvider

  • DaoAuthenticationProvider是借助AuthenticationConfiguration创建的

[SpringSecurity5.6.2源码分析十五]:ProviderManager

  • 相比于AnonymousAuthenticationProvider,此类有完整的认证逻辑

2.2.1 authenticate(..)

  • authenticate(..)是核心方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
         () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
               "Only UsernamePasswordAuthenticationToken is supported"));
   String username = determineUsername(authentication);
   //标准是否在缓存中,默认是
   boolean cacheWasUsed = true;
   //尝试从缓存中获取
   UserDetails user = this.userCache.getUserFromCache(username);
   if (user == null) {
      //标记为缓存中没有此用户
      cacheWasUsed = false;
      try {
         //调用UserDetailsService拿到UserDetails
         user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
      }
      catch (UsernameNotFoundException ex) {
         this.logger.debug("Failed to find user '" + username + "'");
         //是否隐藏异常类型
         if (!this.hideUserNotFoundExceptions) {
            throw ex;
         }
         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);
      //进行密码匹配
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
   }
   catch (AuthenticationException ex) {
      if (!cacheWasUsed) {
         throw ex;
      }
      //到这就说明,进行比较的用户是在缓存中的,那么就从持久化(比如数据库)的地方中读取最新的UserDetails
      //然后再进行密码匹配
      cacheWasUsed = false;
      user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
   }
   //认证后检查器
   this.postAuthenticationChecks.check(user);
   //放入缓存中
   if (!cacheWasUsed) {
      this.userCache.putUserInCache(user);
   }
   Object principalToReturn = user;
   //认证成功后是否将Principal由原来的UserDetails对象转为用户名
   if (this.forcePrincipalAsString) {
      principalToReturn = user.getUsername();
   }
   //创建一个认证成功的认证对象
   return createSuccessAuthentication(principalToReturn, authentication, user);
}
  • 步骤
    • 从缓存中读取UserDetails
    • 如果缓存中没有就从特定的地方(数据库)拿到UserDetails
    • 认证前检查
    • 进行密码匹配
      • 一旦抛出异常并且UserDetails是缓存中的,那就从数据库读取再进行一次密码匹配
    • 认证后检查器
    • 放入缓存中
    • 创建认证对象

2.2.2 retrieveUser(...)

  • retrieveUser(...):从特定的地方拿到UserDetails(比如说数据库) 如果提供的凭据不正确,可以立即抛出AuthenticationException
@Override
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.loadUserByUsername(...)方法

2.2.3 UserDetailsChecker

  • authenticate(..)的执行过程中会使用UserDetailsChecker类,进行认证前后的检查
  • UserDetailsChecker有三个实现类
    • DefaultPreAuthenticationChecks
    • DefaultPostAuthenticationChecks
    • ...

2.2.3.1 DefaultPreAuthenticationChecks

  • DefaultPreAuthenticationChecks:在认证前检查UserDetails是否被锁定,账户是否可用,账户是否过期
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"));
      }
   }
}

2.2.3.2 DefaultPostAuthenticationChecks

  • DefaultPostAuthenticationChecks:在认证后检查密码是否过期
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"));
      }
   }
}

2.2.4 additionalAuthenticationChecks(...)

  • additionalAuthenticationChecks(...):进行密码匹配
/**
 * 进行密码匹配
 * @param userDetails 从某地地方(比如说数据库)读取到的确定的UserDetails
 * @param authentication 通过用户输入的用户名和密码创建的认证对象
 */
@Override
@SuppressWarnings("deprecation")
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"));
   }
   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进行密码匹配

2.2.4.1 DelegatingPasswordEncoder

  • PasswordEncoder的默认实现是DelegatingPasswordEncoder
public final class PasswordEncoderFactories {

   private PasswordEncoderFactories() {
   }

   /**
    * 创建默认的密码密码编码器
    */
   @SuppressWarnings("deprecation")
   public static PasswordEncoder createDelegatingPasswordEncoder() {
      String encodingId = "bcrypt";
      Map<String, PasswordEncoder> encoders = new HashMap<>();
      encoders.put(encodingId, new BCryptPasswordEncoder());
      encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
      encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
      encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
      encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
      encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
      encoders.put("scrypt", new SCryptPasswordEncoder());
      encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
      encoders.put("SHA-256",
            new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
      encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
      encoders.put("argon2", new Argon2PasswordEncoder());
      //可以看到默认使用bcrypt作为密码编码器
      return new DelegatingPasswordEncoder(encodingId, encoders);
   }

}
  • 可以看出SpringSecurity默认是bcrypt作为密码加密算法
  • 使用DelegatingPasswordEncoder作为默认的PasswordEncoder,有如下三个方面的好处
    • 兼容性:可以帮助许多使用旧密码加密的方式的系统顺利的迁移,它允许一个系统有多种不同的加密方式
      • 因为密码是有格式的,比如说密码为123,加密方式为bcrypt,那么加密后的样子可能为{bcrypt}awdmzxc
      • 那我们拿到原来的密码就知道了原来的机密方式,然后使用BCryptPasswordEncoder进行密码匹配,然后就可以利用UserDetailsPasswordService将密码更新为新的密码格式了
    • 便捷性:密码的存储策略不可能一直是某一个数据库,当修改存储策略只需要更改很小一部分就可以实现
    • 稳定性:可以方便对密码加密方案进行升级,升级的情况如下
      • 更换了加密方案
      • 同一个加密方案,比如BCrypt有一个加密强度strength参数,这个发生了改变也会进行升级

2.2.5 createSuccessAuthentication(...)

  • 此方法被DaoAuthenticationProvider重写过的,主要就是提供了升级密码的功能
@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);
}
  • 然后我们看父类的此方法:
    • 此类就是为了创建认证对象,但是出现了一个新的类GrantedAuthoritiesMapper
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
      UserDetails user) {
   //创建认证对象
   UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
         authentication.getCredentials(),
         //这里还会进行权限的映射
         this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
   //更新认证对象的详细信息,通常是一个WebAuthenticationDetails对象
   result.setDetails(authentication.getDetails());
   this.logger.debug("Authenticated user");
   return result;
}

2.2.6 GrantedAuthoritiesMapper

  • 有一种场景哈:比如我们定义A角色有B和C角色,然后管理员用户有A角色,但实际上他是没有B和C角色的
  • 针对此场景GrantedAuthoritiesMapper就是应运而生,此类的原理也很简单无非就是将A -> A + B + C
/**
 * 权限映射接口
 * 比如A角色有B和C的角色,那么{@link org.springframework.security.access.hierarchicalroles.RoleHierarchyAuthoritiesMapper}
 * 就负责将A变成 A,B,C然后保存到用户认证对象中
 */
public interface GrantedAuthoritiesMapper {

   /**
    * 权限转换
    */
   Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities);

}
  • 我们就看一个实现RoleHierarchyAuthoritiesMapper
  • 这里是利用RoleHierarchy(角色继承器)进行转角色的
public class RoleHierarchyAuthoritiesMapper implements GrantedAuthoritiesMapper {

   private final RoleHierarchy roleHierarchy;

   public RoleHierarchyAuthoritiesMapper(RoleHierarchy roleHierarchy) {
      this.roleHierarchy = roleHierarchy;
   }

   @Override
   public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
      return this.roleHierarchy.getReachableGrantedAuthorities(authorities);
   }

}
  • 然后讲下RoleHierarchy的实现RoleHierarchyImpl的转换角色原理
private Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;

private Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
  • rolesReachableInOneStepMap:
    • A -> B
    • B -> C
    • C -> D,E,F
    • 通过如上的结构就可以得出A有B的角色,B有C的角色,C有D,E,F的角色,所有说A有B,C,D,E,F角色
  • rolesReachableInOneOrMoreStepsMap:
    • A -> B,C,D,E,F
    • B -> C,D,E,F
    • C -> D,E,F
    • 同理
  • 通过这两个Map就可以知道角色继承的角色了
转载自:https://juejin.cn/post/7272181868073091084
评论
请登录