Spring Security 验证流程

写在最前面的话

如果想看Security的源码,那么先请看设计模式,这个框架简直就是《大神教你正确使用设计模式的N种姿势》,实际编程如果允许的情况下请用Shiro,而且文章不会对你的编程技巧有所提高,看完只会感觉需要定制自己的逻辑是需要多么的繁琐。

参考资料:
Spring Security Architecture
Spring Security源码分析一:Spring Security认证过程
Spring-Security源码分析-AuthenticationManagerBuilder


Security验证器

1.AuthenticationManagerBuilder继承图

Spring Security 验证流程_第1张图片
AuthenticationManagerBuilder.png

在Security体系中由AuthenticationManager负责对传递进来的用户信息(Security称为AuthenticationToken)和从数据源读取的用户信息(Security称为UserDetails)进行对比验证,而AuthenticationManager是由AuthenticationManagerBuilder负责构建。

看到这里,这是一个典型的建造者模式
AuthenticationConfiguration作为指挥者负责配置和创建Global AuthenticationManagerBuilder,其中会调用AuthenticationManagerBuilder的build()函数构建出AuthenticationManager。

2.AuthenticationManagerBuilder的build()过程

Spring Security 验证流程_第2张图片
AuthenticationManagerBuilderBuild.png

这个图看上去很复杂,但是功能很简单,由AuthenticationManagerBuilder构建出AuthenticationManager,其中会将AuthenticationProviders绑定到AuthenticationManager,那么AuthenticationProviders的作用就是具体从数据源读取UserDetails并和AuthenticationToken的对比过程的代码实现。

注意一点:由AuthenticationConfiguration负责配置是全局AuthenticationManagerBuilder,全局的意思是这个AuthenticationManagerBuilder是放在ApplicationContext容器里。

3.AuthenticationManager验证核心逻辑

Spring Security 验证流程_第3张图片
Authentication.png

这个图看上去又很复杂,逻辑很简单,用户输入的token,交给ProviderManager,然后通过其中的Provider List逐一验证,验证的时候,从数据源加载注册的用户信息,对比成功就返回一个受认证的Token

ProviderManager.class(已简化)

  // AuthenticationManager的实现类,具体的验证流程
  public Authentication authenticate(Authentication authentication) {
    // Token会被传入每一个Provider,由Provider对其验证
    for (AuthenticationProvider provider : getProviders()) {
      // Provider首先会对Token类型进行匹配,如果不匹配则直接调用下一个Provider
      if (!provider.supports(toTest)) {
        continue;
      }

      try {
        result = provider.authenticate(authentication);
        if (result != null) {
          copyDetails(authentication, result);
          break;
        }
      }

    if (result == null && parent != null) {
      // 调用父类的AuthenticationManager再次验证
      try {
        result = parent.authenticate(authentication);
      }
    }
  }

稍微说明一下
Authentication:可以看做是用户输入的username和password的封装Token
Provider:提供Token和UserDetails对比的认证逻辑
UserDetailsService(下文会说到):从数据源读取用户信息并封装成UserDetails


4.短信验证码校验例子

例子很简单,假定短信码已经生成放入缓存并且发送给用户,这里只是演示,verifyCode为空字符串
1.获取用户输入手机号mobile和短信码smsCode
2.获取随机生成的短信码verifyCode
3.从数据源获取用户信息并封装成UserDetails
4.生成成功验证的SmsCodeAuthenticationToken

SmsCodeAuthenticationProvider.class

@Slf4j
public final class SmsCodeAuthenticationProvider implements AuthenticationProvider {
  @Getter @Setter private UserDetailsService userDetailsService;

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;

    // 获取用户输入的手机号和验证码
    String mobile = (String) token.getPrincipal();
    String smsCode = (String) token.getCredentials();
    // 从缓存加载已发送的验证码
    String verifyCode = "";

    // 校验错误则抛出异常
    if (!verifyCode.equals(smsCode)) {
      throw new InternalAuthenticationServiceException("SMS Code Is Not Equal");
    }

    // 从数据源加载用户数据
    UserDetails userDetails = getUserDetailsService().loadUserByUsername(mobile);

    // 生成验证成功的token
    SmsCodeAuthenticationToken authenticatedToken =
        new SmsCodeAuthenticationToken(
            userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());

    // authenticated token details
    authenticatedToken.setDetails(authentication.getDetails());
    return authenticatedToken;
  }

  /** provider只支持SmsCodeAuthenticationToken 类型的校验 */
  @Override
  public boolean supports(Class authentication) {
    return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
  }
}

SmsUserDetailsServiceImpl.class

@Slf4j
@Service(value = "smsUserDetailsServiceImpl")
public class SmsUserDetailsServiceImpl implements UserDetailsService {
  @Autowired private UserService userServiceImpl;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // load user from cache if necessary

    // load user from database
    User user = userServiceImpl.findUserRolePermiByName(username);
    if (user == null) {
      throw new UsernameNotFoundException("User Name " + username + " Not Found");
    }

    // extract permissions
    List authorities =
        user.getRoles().stream()
            .map(item -> new SimpleGrantedAuthority(item.getName()))
            .collect(Collectors.toList());

    return new org.springframework.security.core.userdetails.User(
        user.getName(), user.getPasswd(), authorities);
  }
}

SmsCodeAuthenticationToken.class

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

  private static final long serialVersionUID = 6279233409712693968L;

  /** 手机号 */
  @Getter private final Object principal;

  /** 短信码 */
  @Getter private Object credentials;

  SmsCodeAuthenticationToken(Object principal, Object credentials) {
    super(null);
    this.principal = principal;
    this.credentials = credentials;
    setAuthenticated(false);
  }

  SmsCodeAuthenticationToken(
      Object principal, Object credentials, Collection authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setAuthenticated(true);
  }

  @Override
  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;
  }
}

编程需要做些什么?

看上面认证流程红色部分,对于Security认证添加自定义认证器需要编写的部分
1.Authentication:自定义一个需要被认证Token的类型
2.Provider:自定义一个验证器实例
3.UserDetailsService:自定义用户信息加载服务
然后将UserDetailsService注入到ProviderProvider注入到ProviderManager

1.为什么要自定义UserDetailService?

自带JDBC数据加载,默认会创建一张权限相关的表,但是我们自己有用户数据的表,表结构不一定是我们想要的,而且加载数据是通过JdbcTemplate完成的,但是我们自己加载数据通常通过Mybatis来操作,所以有时候并不太合适。

InMemory适合用在初期测试时用。

LDAP的服务我至少还没有用过。

2.为什么要自定义Provider和Token

当你使用默认的servlet拦截时,比如formLogin的拦截器,拦截之后生成的是UsernamePasswordAuthenticationToken类型的,而使用Provider对Token做验证时是和Token类型匹配的,有时候我们想区分登录类型,或者区别Token类型来从不同的数据源加载用户数据。

当你自定义的UserDetailService作为Bean放到容器里的时候,AuthenticationConfiguration会把它注入到DaoAuthenticationProvider,而DaoAuthenticationProvider默认只支持UsernamePasswordAuthenticationToken类型

下节预告

目前先记住Security的认证流程的大概流程,还没有解释Authentication这个Token是哪里来的,下一篇解释Security对sevrlet的拦截再讲这个问题。

你可能感兴趣的:(Spring Security 验证流程)