美文网首页程序员杂文小品@IT·互联网
Spring Security 源码之 Authenticati

Spring Security 源码之 Authenticati

作者: AlienPaul | 来源:发表于2020-11-13 16:45 被阅读0次

前言

本篇是Spring Security 源码之 UsernamePasswordAuthenticationFilterAuthenticationProvider部分的专项分析。

AuthenticationProvider

顾名思义AuthenticationProvider即认证方式提供者。Spring支持多种认证方式,AuthenticationProvider是这些认证方式统一实现的接口。
常用的AuthenticationProvider子类如下:

  • DaoAuthenticationProvider:从UserDetailsService中获取用户信息进行认证操作。
  • RememberMeAuthenticationProvider:提供RemeberMe方式认证。
  • JwtAuthenticationProvider:JWT方式认证。
  • AnonymousAuthenticationProvider:匿名方式认证

AbstractUserDetailsAuthenticationProvider

在分析DaoAuthenticationProvider之前我们需要先分析下他的父类AbstractUserDetailsAuthenticationProvider

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 要求authentication必须是UsernamePasswordAuthenticationToken类型
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
            () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                    "Only UsernamePasswordAuthenticationToken is supported"));
    // 获取authentication的principal
    String username = determineUsername(authentication);
    // 标记使用了cache
    boolean cacheWasUsed = true;
    // 从userCache中读取user详细信息
    UserDetails user = this.userCache.getUserFromCache(username);
    // 如果cache中没有user详细信息
    if (user == null) {
        // 标记未使用cache
        cacheWasUsed = false;
        try {
            // 获取user详情,retrieveUser方法在子类中实现
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        }
        // 如果报错,认证失败
        catch (UsernameNotFoundException ex) {
            this.logger.debug("Failed to find user '" + username + "'");
            // 这里处理需要隐藏用户名不存在的错误提示这个逻辑
            // 如果不需要隐藏,直接抛出UsernameNotFoundException
            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方法在子类中实现
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    // 如果认证发生异常
    catch (AuthenticationException ex) {
        // 如果没有使用cache,直接抛出异常
        if (!cacheWasUsed) {
            throw ex;
        }
        // There was a problem, so try again after checking
        // we're using latest data (i.e. not from the cache)
        // 如果使用到了cache,尝试不使用cache读取最新数据
        cacheWasUsed = false;
        user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        this.preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    // 进行认证后额外检查
    this.postAuthenticationChecks.check(user);
    // 如果没有使用cache,把用户信息存入cache
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    // 如果强制要求返回string类型,将用户名(String类型)返回
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
    // 创建认证成功的UsernamePasswordAuthenticationToken
    // 使用authoritiesMapper重写用户权限
    // 例如SimpleAuthorityMapper,将authority统一转换为大写或小写,再添加上"ROLE_"前缀
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

DaoAuthenticationProvider

这里我们分析它实现或重写父类的方法。

获取用户的方法retrieveUser

@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    // 如果用户不存在的密码为null,设置为passwordEncoder编码"userNotFoundPassword"后的字符串
    // 用来避免出现安全问题
    // 因为有些passwordEncoder对于空密码会短路处理,执行消耗时间要小于正常密码
    // 用户存在或者不存在执行耗时会有较大差别,为攻击者猜测用户是否存在创造条件
    prepareTimingAttackProtection();
    try {
        // 使用UserDetailsService,获取用户详情
        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) {
        // 抵消上面提到的时间差攻击
        // 用passwordEncoder去编码 userNotFoundEncodedPassword
        // 耗时和编码已存在用户的密码基本一样
        mitigateAgainstTimingAttack(authentication);
        throw ex;
    }
    catch (InternalAuthenticationServiceException ex) {
        throw ex;
    }
    catch (Exception ex) {
        throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    }
}

这段逻辑中出现了2个重要的类:

  • UserDetailsService:负责加载用户数据,接口只有一个方法,从用户名获取用户信息。具有多个实现类。
  • PasswordEncoder:密码编码器。依照安全要求,用户密码是不能明文存放在系统中的,PasswordEncoder正是用于将密码编码处理的工具。具有多种密码编码方式的子类。

这两块儿在后面章节中单独介绍。

额外认证检查additionalAuthenticationChecks,主要校验用户密码是否有误。

@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();
    // 使用passwordEncoder对比encode之后的密码,如果不匹配,抛出异常
    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"));
    }
}

最后是createSuccessAuthentication方法。DaoAuthenticationProvider重写了父类的方法,加入了密码重新encode的逻辑。

@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
        UserDetails user) {
    // 为了安全起见,判断是否需要再次encoding
    boolean upgradeEncoding = this.userDetailsPasswordService != null
            && this.passwordEncoder.upgradeEncoding(user.getPassword());
    // 如果需要,再次encode用户密码
    if (upgradeEncoding) {
        String presentedPassword = authentication.getCredentials().toString();
        String newPassword = this.passwordEncoder.encode(presentedPassword);
        user = this.userDetailsPasswordService.updatePassword(user, newPassword);
    }
    // 再执行父类的createSuccessAuthentication方法,上面我们已经分析过了
    return super.createSuccessAuthentication(principal, authentication, user);
}

UserDetailsService

负责加载用户详细信息,接口只有一个方法loadUserByUsername,即通过username获取UserDetails

它常用的子类如下:

  • CachingUserDetailsService:带缓存的UserDetailsService,先从cache中读取用户详情,如果没读取到,再使用被代理的UserDetailsService读取。
  • InMemoryUserDetailsManager:在内存中维护一系列用户信息,无法持久化,多用于测试环境。
  • JdbcUserDetailsManager:在数据库维护用户信息,可以持久化
  • LdapUserDetailsManager:LDAP协议读取用户信息

PasswordEncoder

PasswordEncoder拥有如下编码算法的子类

  • Argon2PasswordEncoder
  • BCryptPasswordEncoder
  • Pbkdf2PasswordEncoder
  • SCryptPasswordEncoder
  • MessageDigestPasswordEncoder:MD5方式,由于MD5碰撞安全性不高,不推荐使用
  • StandardPasswordEncoder:使用SHA-256方式加上随机的8字节salt,不推荐使用

除此之外,还有2个特殊的实现类:

  • DelegatingPasswordEncoder:代理的PasswordEncoder,在encode之后的密码字符串添加encoder标识前缀,这样在匹配密码的时候,就可以知道编码之后的密码是哪个PasswordEncoder编码的。
  • NoOpPasswordEncoder:不进行任何encode操作

RememberMeAuthenticationProvider

用户使用RememberMe方式进行认证,RememberMe即“记住我”,用户关闭会话后,他的登录状态仍然能够保存一段时间,在此期间不用重新登录。

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 只支持RememberMeAuthenticationToken
    if (!supports(authentication.getClass())) {
        return null;
    }
    // 判断配置的key的hashcode和token携带的keyHash是否相同
    // 如果不相同则认证失败
    if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication).getKeyHash()) {
        throw new BadCredentialsException(this.messages.getMessage("RememberMeAuthenticationProvider.incorrectKey",
                "The presented RememberMeAuthenticationToken does not contain the expected key"));
    }
    return authentication;
}

设置key和创建RememberMeAuthenticationToken的过程在RememberMeAuthenticationFilter中。本人在后续博客中会专门介绍。

JwtAuthenticationProvider

使用JSON Web Token方式进行认证。具体生成和检验逻辑较为复杂。请大家参考JSON Web Token 入门教程

这里仅说明下Spring Security的认证逻辑:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 转换成为bearer token
    BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
    // 使用JwtDecoder解析bearer token
    // 如果解析失败会抛出异常
    Jwt jwt = getJwt(bearer);
    // 将JWT转换为AbstractAuthenticationToken
    AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt);
    // 设置token detail
    token.setDetails(bearer.getDetails());
    this.logger.debug("Authenticated token");
    return token;
}

AnonymousAuthenticationProvider

用于匿名认证。如果某URL允许匿名访问,需要进行此认证过程。

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 只支持AnonymousAuthenticationToken
    if (!supports(authentication.getClass())) {
        return null;
    }
    // 判断配置的key的hashcode和token携带的keyHash是否相同
    // 如果不相同则认证失败 
    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;
}

AnonymousAuthenticationProvider的逻辑和RememberMeAuthenticationProvider非常相似,都是在provider对比key的hash值是否相同。AnonymousAuthenticationToken的生成过程在AnonymousAuthenticationFilter中,以后单独开篇介绍。

本文为原创内容,欢迎大家讨论、批评指正与转载。转载时请注明出处。

相关文章

网友评论

    本文标题:Spring Security 源码之 Authenticati

    本文链接:https://www.haomeiwen.com/subject/ncfbbktx.html