美文网首页程序员杂文小品@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