Spring Security 认证流程分析,带你Debugger

688 阅读8分钟

本文正在参加「金石计划」

日积月累,水滴石穿 😄

前言

对 Spring Security 的文章也写了好几篇了,有基本使用的,有对其进行自定义逻辑的。但那时各位肯定不知道为什么需要这么去定义。所以,本篇呢,来理一理 Spring Security 中默认的表单认证流程源码!

依赖版本

名称版本
spring-boot-starter-parent2.3.12.RELEASE
spring-boot-starter-security2.3.12.RELEASE
spring-security-web5.3.9.RELEASE

调试开始

就加入 Spring Security 依赖后,启动项目,访问登录页面,在页面输入默认的用户名、密码后,点击提交按钮,会发起一个请求类型为 POST,请求路径为 /login 的接口。 该请求会被 Spring Security 的过滤器链代理拦截。

该请求会来到 UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 这个对象是怎么来的呢? 我们可以看看 WebSecurityConfigurerAdapter 抽象类中的 configure(HttpSecurity http) 方法中提供了一个默认的配置,代码如下:

protected void configure(HttpSecurity http) throws Exception {
    logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
​
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .formLogin().and()
        .httpBasic();
}
  • authorizeRequests:允许使用 RequestMatcher 实现(即通过URL模式)基于 HttpServletRequest 限制请求访问。
  • anyRequest().authenticated():请求都需要被认证
  • formLogin:指定支持基于表单的身份验证。如果 FormLoginConfigurer未指定 loginPage,将生成默认登录页。
  • httpBasic:可以配置basic登录

进入 formLogin 方法。

public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
   return getOrApply(new FormLoginConfigurer<>());
}

可以看到这里创建了一个 FormLoginConfigurer对象,FormLoginConfigure是一个用户名密码表单登录的配置类,比如设置登录页面的Url,登录请求的Url 、登录认证成功、认证失败的回调、设置登录请求参数名等等。

public FormLoginConfigurer() {
   super(new UsernamePasswordAuthenticationFilter(), null);
   usernameParameter("username");
   passwordParameter("password");
}

在其无参构造方法中,创建了一个 UsernamePasswordAuthenticationFilter 对象。将其传递给父类,父类会将该过滤器添加到过滤器链中。

AbstractAuthenticationProcessingFilter

上面说到请求会来到 UsernamePasswordAuthenticationFilter,但是它继承了 AbstractAuthenticationProcessingFilter,所以请求会先进入 AbstractAuthenticationProcessingFilter抽象类中,它是一个 Filter,那我们将断点打在 doFilter 方法中。

1666847335730.jpg 介绍一下 chain 中的属性,originalChain 表示原生的过滤器链,也就是 Web Filter;additionalFilters 表示 Spring Security 中的过滤器链;firewalledRequest 表示当前请求;size 表示过滤器链中过滤器的个数;currentPosition 则是过滤器链遍历时候的下标。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {

   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;
   // 进行请求过滤,判断当前调用的请求是否有适配的 xxxAuthenticationFilter 处理
   // 因为 AbstractAuthenticationProcessingFilter 是一个过滤器,那就代表所有的接口请求都会进入该类的 doFilter 方法,
   // 但又不是所有接口都是需要进行登录认证流程的,所以提前进行请求匹配过滤
   if (!requiresAuthentication(request, response)) {
      chain.doFilter(request, response);

      return;
   }

   if (logger.isDebugEnabled()) {
      logger.debug("Request is to process authentication");
   }

  // 身份验证对象
   Authentication authResult;

   try {
     // 调用子类具体实现的 attemptAuthentication(尝试身份验证) 方法
      authResult = attemptAuthentication(request, response);
      if (authResult == null) {
         return;
      }
      // 认证成功时,session 会话需要执行的逻辑
      sessionStrategy.onAuthentication(authResult, request, response);
   }
   catch (InternalAuthenticationServiceException failed) {
   	 // 身份验证失败的默认处理
   	 // 1、清除 SecurityContextHolder
	 // 2、通知配置的RememberMeServices登录失败
	 // 3、将其他行为委托给 AuthenticationFailureHandler,调用其登录失败回调方法(实际开发中会重写AuthenticationFailureHandler接口)
      unsuccessfulAuthentication(request, response, failed);
      return;
   }
   catch (AuthenticationException failed) {
      // Authentication failed
      unsuccessfulAuthentication(request, response, failed);
      return;
   }

   // 认证成功
   // 在successfulAuthentication执行前继续执行过滤器(默认为 false)
   if (continueChainBeforeSuccessfulAuthentication) {
      chain.doFilter(request, response);
   }
 	// 身份验证成功的处理
    // 1、在SecurityContextHolder上设置成功的身份验证对象
	// 2、通知配置的RememberMeServices成功登录
	// 3、通过配置的ApplicationEventPublisher激发InteractiveAuthenticationSuccessEvent,身份验证成功的事件回调
	// 4、将其他行为委托给AuthenticationSuccessHandler,调用其登录成功回调方法(实际开发中会重写AuthenticationSuccessHandler接口)
   successfulAuthentication(request, response, chain, authResult);
}

上面逻辑很简单,先调用 attemptAuthentication 方法进行身份验证,如果认证失败则对其对应的处理,认证成功也进行对应的处理。那核心的认证逻辑就是在 attemptAuthentication 方法中了,该方法可以由子类实现。

UsernamePasswordAuthenticationFilter

image.png

public class UsernamePasswordAuthenticationFilter extends
      AbstractAuthenticationProcessingFilter {
​
    // 默认的参数名称,可以通过其set方法进行自定义
   public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
   public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
​
   private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
   private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
   // 是否仅仅post方式
   private boolean postOnly = true;
​
   // 对请求进行过滤,只有接口为 /login,请求方式为 POST,才会进入逻辑
   // 在其父类的 requiresAuthentication 方法会进行匹配
   public UsernamePasswordAuthenticationFilter() {
      super(new AntPathRequestMatcher("/login", "POST"));
   }
​
   public Authentication attemptAuthentication(HttpServletRequest request,
         HttpServletResponse response) throws AuthenticationException {
      if (postOnly && !request.getMethod().equals("POST")) {
         throw new AuthenticationServiceException(
               "Authentication method not supported: " + request.getMethod());
      }
     // 获得参数值
      String username = obtainUsername(request);
      String password = obtainPassword(request);
​
      if (username == null) {
         username = "";
      }
​
      if (password == null) {
         password = "";
      }
​
      username = username.trim();
     // 使用请求参数传递的用户名和密码,封装一个未认证 UsernamePasswordAuthenticationToken(用户名密码身份验证令牌) 对象,
     // 然后将该对象交给 provider 进行授权
      UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
            username, password);
      // 设置该次请求信息,比如:调用地址、sessionId。
      setDetails(request, authRequest);
     // 获得父类的 AuthenticationManager,调用 authenticate 方法进行认证 
      return this.getAuthenticationManager().authenticate(authRequest);
   }
    
}

封装 UsernamePasswordAuthenticationToken

image.png

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

   private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

   // 用户名
   private final Object principal;
   // 密码
   private Object credentials;

   
   public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
      super(null);
      this.principal = principal;
      this.credentials = credentials;
      // 设置是否已认证状态为 false 
      setAuthenticated(false);
   }

  
   public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
         Collection<? extends GrantedAuthority> authorities) {
      super(authorities);
      this.principal = principal;
      this.credentials = credentials;
      // 调用父类的 setAuthenticated 方法,设置是否已认证状态为 true 
      super.setAuthenticated(true);
   }

    // 该方法只能设置是否已认证状态为 false 
   public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
      if (isAuthenticated) {
         throw new IllegalArgumentException(
               "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
      }

      super.setAuthenticated(false);
   }
    
   // 密码擦除
   @Override
   public void eraseCredentials() {
      super.eraseCredentials();
      credentials = null;
   }
}

AuthenticationManager

认证管理器,AuthenticationManager 是认证相关的核心接口,是发起认证的入口,用于处理认证请求,接口只提供了一个认证方法,方法接收一个未通过认证 Authentication 对象,返回一个通过认证的 Authentication 对象。最常见的实现是ProviderManager

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

ProviderManager

image.png 在梳理具体的认证流程前,我们先看一下 Debugger 页面的 this。讲一下里面的两个属性:

  • providers 属性是一个类型为 AuthenticationProvider 的列表。该属性可以存放多种认证方式,列表中的每种认证方式都将会被尝试认证。 现在列表中只有一个 Provider,类型为 AnonymousAuthenticationProvider

  • parent 属性的类型是 ProviderManager,该属性就是一个 AuthenticationManager,其中 providers 列表有一个类型为 DaoAuthenticationProviderProvider

那可以知道ProviderManager管理了众多的 AuthenticationProvider实例。在一次完整的认证流程中,可能会同时存在多个AuthenticationProvider,同时,ProviderManager还具有一个可选的 parent 属性,该属性可能为 null,如果当前 ProviderManager 中所有的AuthenticationProvider都认证失败,那么就会调用parent进行认证。

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;
   boolean debug = logger.isDebugEnabled();

   // 遍历当前 ProviderManager 对象中的 providers
   // 每个 AuthenticationProvider 都将与 toTest 未认证的身份认证对象进行匹配
   for (AuthenticationProvider provider : getProviders()) {
      if (!provider.supports(toTest)) {
         continue;
      }

      if (debug) {
         logger.debug("Authentication attempt using "
               + provider.getClass().getName());
      }
      try {
         // 调用对应的 AuthenticationProvider类的 authenticate 方法。
         result = provider.authenticate(authentication);
         // result不等于 null,说明认证成功,循环结束不再执行后续的 AuthenticationProvider
         if (result != null) {
            copyDetails(authentication, result);
            break;
         }
      }
      // 某个 AuthenticationProvider 认证出现异常,并不会结束,会将该异常暂存,继续执行下一个 AuthenticationProvider 认证流程
      catch (AccountStatusException | InternalAuthenticationServiceException e) {
         prepareException(e, authentication);
         throw e;
      } catch (AuthenticationException e) {
         lastException = e;
      }
   }

   if (result == null && parent != null) {
      try {
          // 如果当前 AuthenticationProvider 列表中的 Provider 都认证失败,那么使用 parent,也就是父 AuthenticationManager 继续认证
         result = parentResult = parent.authenticate(authentication);
      }
      catch (ProviderNotFoundException e) {
      }
      catch (AuthenticationException e) {
         lastException = parentException = e;
      }
   }
  // 认证成功
   if (result != null) {
      if (eraseCredentialsAfterAuthentication
            && (result instanceof CredentialsContainer)) {
         // 身份验证已完成。擦除密码和其他机密数据
         ((CredentialsContainer) result).eraseCredentials();
      }
     //尝试认证并认证成功,则将发布 AuthenticationSuccessEvent 事件
	 //如果父 AuthenticationManager 已发布了 AuthentiationSuccessEvent 事件,则该判断可防止其重复发布事件
      if (parentResult == null) {
         eventPublisher.publishAuthenticationSuccess(result);
      }
      return result;
   }

   // 父级为空,或引发认证异常
   if (lastException == null) {
      lastException = new ProviderNotFoundException(messages.getMessage(
            "ProviderManager.providerNotFound",
            new Object[] { toTest.getName() },
            "No AuthenticationProvider found for {0}"));
   }
   //尝试认证并认证失败,则将发布 AuthenticationEventPublisher 事件
   //如果父 AuthenticationManager 已发布了 AuthenticationEventPublisher 事件,则该判断可防止其重复发布事件
   if (parentException == null) {
      prepareException(lastException, authentication);
   }

   throw lastException;
}

AuthenticationProvider

AuthenticationProvider(身份验证提供者),可以将多个AuthenticationProvider实例添加到ProviderManager中。其每个AuthenticationProvider可以执行特定的 Authentication (身份验证)类型。例如:DaoAuthenticationProvider支持基于用户名+密码的 UsernamePasswordAuthenticationToken 身份验证。也可以自定义认证方式,比如:EmailVerificationCodeAuthenticationProvider支持邮箱 + 验证码的 EmailVerificationCodeAuthenticationToken 身份验证。

public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);
}
  • authenticate() 方法接收一个未通过认证 Authentication 对象,返回一个通过认证的 Authentication 对象。可以实现 authenticate() 方法来自定义身份验证逻辑。

  • supports(Class<?> authentication)方法接收一个 Authentication(身份验证) 对象,如果 AuthenticationProvider 支持指定的身份验证对象,则返回 true。 但返回 true并不保证 AuthenticationProvider 能够对提供的 Authentization 类实例进行正确身份验证;它只是表明它可以对其进行更深入的验证。authenticate方法可以返回 null,尝试其他的 AuthentitationProvider 进行验证。

AbstractUserDetailsAuthenticationProvider

从上面的截图可以知道,用户名密码流程的认证会交给 DaoAuthenticationProvider 进行处理,但是再看DaoAuthenticationProvider 的源码前,我们需要先看 AbstractUserDetailsAuthenticationProvider 类的源码,因为它是 DaoAuthenticationProvider 的父类。而且 authenticate、supports 方法在其父类中。

public abstract class AbstractUserDetailsAuthenticationProvider implements
      AuthenticationProvider, InitializingBean, MessageSourceAware {
​
   // 交给子类进行实现,可以进行其他身份验证检查
   protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
         UsernamePasswordAuthenticationToken authentication)
         throws AuthenticationException;
    
// 认证方法 
   public Authentication authenticate(Authentication authentication)
         throws AuthenticationException {
      Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
            () -> messages.getMessage(
                  "AbstractUserDetailsAuthenticationProvider.onlySupports",
                  "Only UsernamePasswordAuthenticationToken is supported"));
​
      //获取用户名
      String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
            : authentication.getName();
​
      boolean cacheWasUsed = true;
      // 根据用户名从缓存中获得UserDetails对象
      UserDetails user = this.userCache.getUserFromCache(username);
​
      if (user == null) {
         cacheWasUsed = false;
​
         try {
             // 如果缓存中没有信息,通过子类 DaoAuthenticationProvider 实现的 retrieveUser 方法,返回一个 UserDetails 对象
            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;
            }
         }
​
         Assert.notNull(user,
               "retrieveUser returned null - a violation of the interface contract");
      }
​
      try {
          // 检查该用户对象的各种状态,比如:账户是否未锁定、账户是否启用、账户是否未过期
         preAuthenticationChecks.check(user);
         // 使用子类 DaoAuthenticationProvider 实现的 additionalAuthenticationChecks 方法,检查密码是否输入正确
         additionalAuthenticationChecks(user,
               (UsernamePasswordAuthenticationToken) authentication);
      }
      catch (AuthenticationException exception) {
         if (cacheWasUsed) {
            // 该用户信息可能来源于缓存,缓存中的信息可能是错误的、老旧的,重新通过子类 DaoAuthenticationProvider 实现的 retrieveUser 方法加载用户信息
            cacheWasUsed = false;
            user = retrieveUser(username,
                  (UsernamePasswordAuthenticationToken) authentication);
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                  (UsernamePasswordAuthenticationToken) authentication);
         }
         else {
            throw exception;
         }
      }
    // 检查该用户对象的各种状态,比如:凭证(密码)是否未过期
      postAuthenticationChecks.check(user);
    // 存入缓存
      if (!cacheWasUsed) {
         this.userCache.putUserInCache(user);
      }

      Object principalToReturn = user;

      if (forcePrincipalAsString) {
         principalToReturn = user.getUsername();
      }
      // 会调用子类方法,设置是否已认证为true,设置权限信息,到此,认证流程就完成了。
      return createSuccessAuthentication(principalToReturn, authentication, user);
   }


   // 创建认证成功的 Authentication 对象,子类可以进行重写
   protected Authentication createSuccessAuthentication(Object principal,
         Authentication authentication, UserDetails user) {
   
      UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
            principal, authentication.getCredentials(),
            authoritiesMapper.mapAuthorities(user.getAuthorities()));
      result.setDetails(authentication.getDetails());
​
      return result;
   }
​
​
   //允许子类从特定的方式加载 UserDetails
   protected abstract UserDetails retrieveUser(String username,
         UsernamePasswordAuthenticationToken authentication)
         throws AuthenticationException;
​
    // 验证传入的身份验证对象是否是 UsernamePasswordAuthenticationToken。如果是则返回 true。
   public boolean supports(Class<?> authentication) {
      return (UsernamePasswordAuthenticationToken.class
            .isAssignableFrom(authentication));
   }
}

DaoAuthenticationProvider

DaoAuthenticationProvider 重写了父类的additionalAuthenticationChecksretrieveUsercreateSuccessAuthentication方法。

// 检查密码是否输入正确
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"));
    }
}
​
// 检索用户
protected final UserDetails retrieveUser(String username,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        // 通过调用 UserDetailsService 的 loadUserByUsername 方法加载用户信息
        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);
    }
}

// 创建认证成功的 Authentication 对象
@Override
protected Authentication createSuccessAuthentication(Object principal,
                                                     Authentication authentication, UserDetails user) {
    // 是否需要再次进行密码加密,默认实现为 false,具体看提供的实现类
    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);
}

总结

  • 输入用户名密码,发起请求会来到 AbstractAuthenticationProcessingFilterdoFilter 方法
  • 在方法内部进行请求 url 匹配,如果是认证请求则执行认证相关逻辑。
  • 调用 UsernamePasswordAuthenticationFilterattemptAuthentication 方法,在 attemptAuthentication 方法内部,根据传入的用户名密码参数构建一个未通过认证的 UsernamePasswordAuthenticationToken 认证对象。
  • 将未通过认证的 UsernamePasswordAuthenticationToken 认证对象传入到AuthenticationManagerauthenticate 方法中,AuthenticationManager 的具体实现为 ProviderManager。在其方法内部进行认证方式匹配provider.supports(toTest),匹配成功后调用具体身份验证实现,用户名密码认证的实现为 DaoAuthenticationProvider
  • DaoAuthenticationProvider 类中,根据用户名加载用户、检查用户状态、构造通过认证的身份验证对象。

扩展点

通过对认证流程的梳理,可以发现几个可扩展点。

  • 可以 extends AbstractAuthenticationProcessingFilter,自定义其他认证方式,比如手机号、邮箱验证码登录。
  • 可以 extends AbstractAuthenticationToken,自定义身份验证对象。
  • 可以 implements AuthenticationProvider,自定义身份验证实现,通过手机号、邮箱查询用户信息。

  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞 + 收藏。