Bootstrap

Spring Security认证流程

前言

Spring Seuciry相关的内容看了实在是太多了,但总觉得还是理解地不够巩固,还是需要靠知识输出做巩固。

过滤器链和认证过程

一个认证过程,其实就是过滤器链上的一个绿色矩形Filter所要执行的过程。

基本的认证过程有三步骤:

要理解这个过程,可以从类,,和(实现类,由默认配置提供)进行了解。只要创建一个含有的springboot项目,在适当地打上断点接口看到这个流程。

用认证部门进行讲解

)

请求到之后,负责该请求的会将请求的内容封装为一个对象交给,仅管理,不做具体的认证操作,具体的操作由与该相关的进行处理。当然,每个需要判断是否为该部门负责,是则由该部门负责处理,否则交给下一个部门处理。认证成功之后会创建一个认证通过的返回。否则要么抛出异常表示认证不通过,要么交给下一个部门处理。

如果需要新增认证类型,只要增加相应的和与该想对应的就即可,当然也可以增加一个与已有前台对应的。会通过生成的来判断该认证是否由该部门负责,因而也许提供一个两者相互认同的.

需要人员资料时,则可以从获取。不同的系统有不同的,需要我们提供该,否则将拿到空白档案。当然,不一定是唯一的,可以有自己的专属。

上图还可以有如下的画法:

这个画法可能会和FilterChain更加符合。每一个前台其实就是FilterChain中的一个,客户拿着请求逐个前台请求认证,找到正确的前台之后进行认证判断。

前台(Filter)

这里的仅仅指实现认证的Filter,Spring Security Filter Chain中处理这些Filter还有其他的Filter,比如。如果非要给角色给他们,那么就当他们是吧。

Spring Security为我们提供了3个已经实现的Filter。,和 。如果不做任何个性化的配置,和会在默认的过滤器链中。这两种认证方式也就是默认的认证方式。

仅仅会对路径生效,也就是说负责发布认证,发布认证的接口为。

public class UsernamePasswordAuthenticationFilter extends
    AbstractAuthenticationProcessingFilter {
  ...
  public UsernamePasswordAuthenticationFilter() {
    super(new AntPathRequestMatcher("/login", "POST"));
  }
  ...
}

为抽象类的一个实现,而为抽象类的一个实现。这四个类的源码提供了不错的实现思路。

AbstractAuthenticationProcessingFilter

提供了认证前后需要做的事情,其子类只需要提供实现完成认证的抽象方法即可。使用时,需要提供一个拦截路径(使用进行匹配)来拦截对应的特定的路径。

UsernamePasswordAuthenticationFilter

作为实际的前台,会将客户端提交的username和password封装成一个交给进行认证。如此,她的任务就完成了。

BasicAuthenticationFilter

该只会处理含有的Header,且小写化后的值以开头的请求,否则该不负责处理。该Filter会从header中获取Base64编码之后的username和password,创建提供给进行认证。

认证资料(Authentication)

接到请求之后,会从请求中获取所需的信息,创建自家所认识的,则主要是通过的类型判断是否由该部门处理。

public interface Authentication extends Principal, Serializable {
  
  // 该principal具有的权限。AuthorityUtils工具类提供了一些方便的方法。
  Collection getAuthorities();
  // 证明Principal的身份的证书,比如密码。
  Object getCredentials();
  // authentication request的附加信息,比如ip。
  Object getDetails();
  // 当事人。在username+password模式中为username,在有userDetails之后可以为userDetails。
  Object getPrincipal();
  // 是否已经通过认证。
  boolean isAuthenticated();
  // 设置通过认证。
  void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

在被认证之后,会保存到一个thread-local的SecurityContext中。

// 设置
SecurityContextHolder.getContext().setAuthentication(anAuthentication);
// 获取
Authentication existingAuth = SecurityContextHolder.getContext()
        .getAuthentication();

在写的时候,可以先检查中是否已经存在通过认证的了,如果存在,则可以直接跳过该Filter。已经通过验证的建议设置为一个不可修改的实例。

目前从的类图中看到的实现类,均为的抽象子类的实现类。实现类有好几个,与前面的讲到的Filter相关的有和。

为和的子类。实现了一些简单的方法,但主要的方法还需要实现。该类的方法的实现可以看到常用的principal类为、和。如果有需要将对象设置为principal,可以考虑继承这三个类中的一个。

public String getName() {
  if (this.getPrincipal() instanceof UserDetails) {
    return ((UserDetails) this.getPrincipal()).getUsername();
  }
  if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
    return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
  }
  if (this.getPrincipal() instanceof Principal) {
    return ((Principal) this.getPrincipal()).getName();
  }

  return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
}

认证管理部门(AuthenticationManager)

是一个接口,认证,如果认证通过之后,返回的应该带上该principal所具有的。

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

该接口的注释中说明,必须按照如下的异常顺序进行检查和抛出:

Spring Security提供一个默认的实现。仅执行管理职能,具体的认证职能由执行。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
    InitializingBean {
  ...

  public ProviderManager(List providers) {
    this(providers, null);
  }

  public ProviderManager(List providers,
      AuthenticationManager parent) {
    Assert.notNull(providers, "providers list cannot be null");
    this.providers = providers;
    this.parent = parent;
    checkState();
  }

  public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
    Class toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;
    boolean debug = logger.isDebugEnabled();

    for (AuthenticationProvider provider : getProviders()) {
      // #1, 检查是否由该认证部门进行认证`AuthenticationProvider`
      if (!provider.supports(toTest)) {
        continue;
      }

      if (debug) {
        logger.debug("Authentication attempt using "
            + provider.getClass().getName());
      }

      try {
        // #2, 认证部门进行认证
        result = provider.authenticate(authentication);

        if (result != null) {
          copyDetails(authentication, result);
          // #3,认证通过则不再进行下一个认证部门的认证,否则抛出的异常被捕获,执行下一个认证部门(AuthenticationProvider)
          break;
        }
      }
      catch (AccountStatusException e) {
        prepareException(e, authentication);
        // SEC-546: Avoid polling additional providers if auth failure is due to
        // invalid account status
        throw e;
      }
      catch (InternalAuthenticationServiceException e) {
        prepareException(e, authentication);
        throw e;
      }
      catch (AuthenticationException e) {
        lastException = e;
      }
    }

    if (result == null && parent != null) {
      // Allow the parent to try.
      try {
        result = parentResult = parent.authenticate(authentication);
      }
      catch (ProviderNotFoundException e) {
        // 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 e) {
        lastException = parentException = e;
      }
    }
    // #4, 如果认证通过,执行认证通过之后的操作
    if (result != null) {
      if (eraseCredentialsAfterAuthentication
          && (result instanceof CredentialsContainer)) {
        // Authentication is complete. Remove credentials and other secret data
        // from authentication
        ((CredentialsContainer) result).eraseCredentials();
      }

      // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
      // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
      if (parentResult == null) {
        eventPublisher.publishAuthenticationSuccess(result);
      }
      return result;
    }

    // Parent was null, or didn't authenticate (or throw an exception).
    // #5,如果认证不通过,必然有抛出异常,否则表示没有配置相应的认证部门(AuthenticationProvider)
    if (lastException == null) {
      lastException = new ProviderNotFoundException(messages.getMessage(
          "ProviderManager.providerNotFound",
          new Object[] { toTest.getName() },
          "No AuthenticationProvider found for {0}"));
    }

    // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
    // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
    if (parentException == null) {
      prepareException(lastException, authentication);
    }

    throw lastException;
  }
  ...
}

当使用到Spring Security OAuth2的时候,会看到另一个实现。

认证部门(AuthenticationProvider)

负责实际的认证工作,与协同工作。也许其他的并不需要的协作。

public interface AuthenticationProvider {
  // 进行认证
  Authentication authenticate(Authentication authentication)
      throws AuthenticationException;
  // 是否由该AuthenticationProvider进行认证
  boolean supports(Class authentication);
}

该接口有很多的实现类,其中包含了(直接AuthenticationProvider)和(通过简介继承)。这里重点讲讲和。

AbastractUserDetailsAuthenticationProvider

顾名思义,是对支持的Provider,其他的Provider,如RememberMeAuthenticationProvider就不需要用到。该抽象类有两个抽象方法需要实现类完成:

// 获取 UserDetails
protected abstract UserDetails retrieveUser(String username,
    UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException;

protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
    UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException;

方法为校验提供。先看下UserDetails:

public interface UserDetails extends Serializable {
  
  Collection getAuthorities();

  String getPassword();
  
  String getUsername();
  // 账号是否过期
  boolean isAccountNonExpired();
  // 账号是否被锁
  boolean isAccountNonLocked();
  // 证书(password)是否过期
  boolean isCredentialsNonExpired();
  // 账号是否可用
  boolean isEnabled();
}

分为三步验证:

的默认实现为,负责完成校验:

的默认实现为,负责完成校验:

需要由实现类完成。

校验成功之后,会创建并返回一个通过认证的。

protected Authentication createSuccessAuthentication(Object principal,    Authentication authentication, UserDetails user) {  // Ensure we return the original credentials the user supplied,  // so subsequent attempts are successful even with encoded passwords.  // Also ensure we return the original getDetails(), so that future  // authentication events after cache expiry contain the details  UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(      principal, authentication.getCredentials(),      authoritiesMapper.mapAuthorities(user.getAuthorities()));  result.setDetails(authentication.getDetails());  return result;}

DaoAuthenticationProvider

如下为对抽象方法的实现。

// 检查密码是否正确
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"));
  }
}
// 通过资料室(UserDetailsService)获取UserDetails对象
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;
  }
  ...
}

在以上的代码中,需要提供和实例。只要实例化这两个类,并放入到Spring容器中即可。

资料部门(UserDetailsService)

接口提供认证过程所需的的类,如需要一个实例。

public interface UserDetailsService {
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Spring Security提供了两个的实现:和。为默认配置,从的配置中可以看出。当然也不容易理解,基于数据库的实现需要增加数据库的配置,不适合做默认实现。这两个类均为的实现类,定义了的CRUD操作。使用做存储。

public interface UserDetailsManager extends UserDetailsService {
  void createUser(UserDetails user);

  void updateUser(UserDetails user);

  void deleteUser(String username);

  void changePassword(String oldPassword, String newPassword);

  boolean userExists(String username);
}

如果我们需要增加一个,可以考虑实现或者。

增加一个认证流程

到这里,我们已经知道Spring Security的流程了。从上面的内容可以知道,如要增加一个新的认证方式,只要增加一个[ + + ]组合即可。事实上,不是必须的,可根据需要实现。

我会在另一篇文章中以手机号码+验证码登录为例进行讲解。