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

前言

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

1. authenticate(...)

  • authenticate是ProviderManager的核心方法,也是入口方法
java 复制代码
@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的实现类比较多,现只介绍默认注册的,其他的会随着对应的过滤器进行介绍
java 复制代码
public interface AuthenticationProvider {

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

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

}

2.1 AnonymousAuthenticationProvider

  • 由AnonymousConfigurer负责注册,AnonymousConfigurer也是默认注册的配置类
  • 分析源码看出无非是判断传入的AnonymousAuthenticationToken中的key是否是正确的
java 复制代码
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的源码
java 复制代码
@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);
}
  • 分析上图和AnonymousAuthenticationFilter的源码就可以得出AnonymousAuthenticationToken是SpringSecurity的一个保底策略
  • 确保我们使用SecurityContextHolder.getContext().getAuthentication()至少有一个对象

2.2 DaoAuthenticationProvider

  • DaoAuthenticationProvider是借助AuthenticationConfiguration创建的
  • 相比于AnonymousAuthenticationProvider,此类有完整的认证逻辑

2.2.1 authenticate(..)

  • authenticate(..)是核心方法
java 复制代码
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
java 复制代码
@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是否被锁定,账户是否可用,账户是否过期
java 复制代码
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:在认证后检查密码是否过期
java 复制代码
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(...):进行密码匹配
java 复制代码
/**
 * 进行密码匹配
 * @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
java 复制代码
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重写过的,主要就是提供了升级密码的功能
java 复制代码
@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
java 复制代码
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
java 复制代码
/**
 * 权限映射接口
 * 比如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(角色继承器)进行转角色的
java 复制代码
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的转换角色原理
java 复制代码
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就可以知道角色继承的角色了
相关推荐
用户8307196840829 小时前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解9 小时前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解10 小时前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记13 小时前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者1 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840821 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解1 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者2 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺2 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端
Derek_Smart2 天前
从一次 OOM 事故说起:打造生产级的 JVM 健康检查组件
java·jvm·spring boot