美文网首页
spring security AuthenticationM

spring security AuthenticationM

作者: do_young | 来源:发表于2022-02-09 10:40 被阅读0次

    AuthenticationManager相关类图

    • AuthenticationManager验证过程
      AuthenticationManager验证过程涉及到的类和接口较多,我们就从这里开始逐一分析,首先我手画了一张图作为索引,这张图说明了各个类和接口之间的关系。
      [图片上传失败...(image-31deef-1606271028193)]

    • AuthenticationManager 为认证管理接口类,其定义了认证方法authenticate()

    • ProviderManager 为验证管理类,实现了接口AuthenticationManager ,并在认证方法authenticate() 中将身份认证委托给具有认证资格的AuthenticationProvider 进行身份认证。
      [图片上传失败...(image-263396-1606271028193)]

    从上图中我们可以看到AuthenticationManager的实现类有很多,至于为什么我只提及到ProviderManager,有时间的小伙伴可以进行源码跟踪就能发现。

    ProviderManager的成员变量

    • 关于AuthenticationEventPublisher不懂的小伙伴可以查看Security中的认证事件发布器
    • providers存储了一个 AuthenticationProvider 类型的list。和Security中的配置文件相对应。
    • MessageSourceAccessor一个国际化消息来源访问器,Security中用于信息提示。

    AuthenticationProvider

    • 接口认证类,定义了认证方法authenticate()
    • AbstractUserDetailsAuthenticationProvider 为认证抽象类,实现了接口 AuthenticationProvider 定义的认证方法 authenticate()。还定义了抽象方法 retrieveUser() 用于查询数据库用户信息,以及抽象方法 additionalAuthenticationChecks() 用作额外的身份验证检查。

    DaoAuthenticationProvider

    • 继承自抽象类AbstractUserDetailsAuthenticationProvider,实现了该类的方法 retrieveUser()additionalAuthenticationChecks()
    • DaoAuthenticationProvider 中还具有四个成员变量,分别是
    • USER_NOT_FOUND_PASSWORD
      顾名思义,该变量是与PasswordEncoder一同使用的,当Security未找到用户时,用于PasswordEncoder.matches()执行的明文密码,以防止恶意用户确定用户名是否有效的旁路攻击的可能性。
    • PasswordEncoder
      密码编码器,Security中的主要作用是用于将明文密码转换成密文,它采用SHA-256算法,迭代1024次,使用一个密钥(site-wide secret)以及8位随机盐对原密码进行加密。
    • userNotFoundEncodedPassword
      同上方USER_NOT_FOUND_PASSWORD一致,只不过Security将其修饰为volatile的,确保了该变量不会因为编译器的优化而被省略。
      关于volatile关键字不懂的请查看volatile理解
    • UserDetailsService
      这个变量相信很多人都知道,不做过多的解释,Security中用于查询用户详细信息的接口
    • UserDetailsPasswordService
      顾名思义,该接口用于修改用户的密码,只有在使用持久存储库时才有效,基于内存的方式会抛异常。(用处不大,只做了解即可)

    流程分析

    [图片上传失败...(image-e26d3f-1606271028194)]

    [图片上传失败...(image-3f01f0-1606271028194)]

    static final class AuthenticationManagerDelegator implements AuthenticationManager {
            private AuthenticationManagerBuilder delegateBuilder;
            private AuthenticationManager delegate;
            private final Object delegateMonitor = new Object();
    
            AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder) {
                Assert.notNull(delegateBuilder, "delegateBuilder cannot be null");
                this.delegateBuilder = delegateBuilder;
            }
    
            @Override
            public Authentication authenticate(Authentication authentication)
                    throws AuthenticationException {
                if (this.delegate != null) {
                    return this.delegate.authenticate(authentication);
                }
    
                synchronized (this.delegateMonitor) {
                    if (this.delegate == null) {
                        this.delegate = this.delegateBuilder.getObject();
                        this.delegateBuilder = null;
                    }
                }
    
                return this.delegate.authenticate(authentication);
            }
    
            @Override
            public String toString() {
                return "AuthenticationManagerDelegator [delegate=" + this.delegate + "]";
            }
        }
    
    

    1、Security认证的入口为AuthenticationManager的authenticate()方法,从上面代码中我们可以看出,AuthenticationManagerDelegator使用了单例模式来防止AuthenticationManager在初始化时发生无限递归,因此我们只分析上方的两个实现类OAuth2AuthenticationManager和ProviderManager。

    OAuth2AuthenticationManager

    OAuth2AuthenticationManager的authenticate()的方法代码如下:

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
            if (authentication == null) {
                throw new InvalidTokenException("Invalid token (token not found)");
            }
    (1)     String token = (String) authentication.getPrincipal();
    (2)     OAuth2Authentication auth = tokenServices.loadAuthentication(token);
            if (auth == null) {
                throw new InvalidTokenException("Invalid token: " + token);
            }
    
            Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
            if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
                throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
            }
    
    (3)     checkClientDetails(auth);
    
            if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
                OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
                // Guard against a cached copy of the same details
                if (!details.equals(auth.getDetails())) {
                    // Preserve the authentication details from the one loaded by token services
                    details.setDecodedDetails(auth.getDetails());
                }
            }
            auth.setDetails(authentication.getDetails());
            auth.setAuthenticated(true);
            return auth;
        }
    
    private void checkClientDetails(OAuth2Authentication auth) {
            if (clientDetailsService != null) {
                ClientDetails client;
                try {
                                client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
                }
                catch (ClientRegistrationException e) {
                    throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
                }
                Set<String> allowed = client.getScope();
                for (String scope : auth.getOAuth2Request().getScope()) {
                    if (!allowed.contains(scope)) {
                        throw new OAuth2AccessDeniedException(
                                "Invalid token contains disallowed scope (" + scope + ") for this client");
                    }
                }
            }
        }
    
    

    OAuth2AuthenticationManager用于集成了OAuth2.0时使用的,如果没有用到,可以忽略。
    其中:

    • (1)处的代码,期望传入的身份验证请求具有一个主体值,该主体值是一个访问令牌值(一般在(authorization header)请求头中)
    • (2)处从ResourceServerTokenServices通过查询数据库中 oauth_client_details该表,加载身份验证。
    • 通过(3)处检查资源id是否包含在授权请求中。检查通过之后封装OAuth2认证实体返回给UsernamePasswordAuthenticationFilter以确定认证成功或失败。

    ProviderManager

    ProviderManager的authenticate()的方法代码如下:

    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();
    
    (1)     for (AuthenticationProvider provider : getProviders()) {
                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);
                        break;
                    }
                }
                catch (AccountStatusException | InternalAuthenticationServiceException e) {
                    prepareException(e, authentication);
                    // SEC-546: Avoid polling additional providers if auth failure is due to
                    // invalid account status
                    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;
                }
            }
    
            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).
    
            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;
        }
    
    

    其中:

    • (1)处的代码从ProviderManager的属性providers[List<authenticationprovider style="margin: 0px; padding: 0px;">]</authenticationprovider>中通过for循环拿到支持该类认证的AuthenticationProvider用于认证处理。
    • (2)处的代码,对用户进行身份认证,认证过程如下所示。

    在上面(2)处的代码,使用了AbstractUserDetailsAuthenticationProvider的authenticate()方法,接下来具体分析该方法,代码如下:

    public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                    () -> messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.onlySupports",
                            "Only UsernamePasswordAuthenticationToken is supported"));
    
            // Determine username
            String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                    : authentication.getName();
    
            boolean cacheWasUsed = true;
            UserDetails user = this.userCache.getUserFromCache(username);
    
            if (user == null) {
                cacheWasUsed = false;
    
                try {
    (1)             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);
    (2)         additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (AuthenticationException exception) {
                if (cacheWasUsed) {
                    // There was a problem, so try again after checking
                    // we're using latest data (i.e. not from the cache)
                    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();
            }
    
    (3)     return createSuccessAuthentication(principalToReturn, authentication, user);
        }
    
    
    • (3)处创建一个成功的身份认证令牌并将用户认证信息其放置到UsernamePasswordAuthenticationToken中。

      查看源码我们得知,AbstractUserDetailsAuthenticationProvider的(1)处和(2)处调用的方法没有具体的实现,因此我们接下来分析它的子类DaoAuthenticationProvider

    • retrieveUser()

    protected final UserDetails retrieveUser(String username,
                UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            prepareTimingAttackProtection();
            try {
    (1)         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);
            }
        }
    
    

    (1)处调用DaoAuthenticationProvider成员变量UserDetailsService的方法loadUserByUsername()从数据库中加载用户详细信息(用过Security的对此处应该是很熟悉了)

    • additionalAuthenticationChecks()
    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"));
            }
    
    (1)     String presentedPassword = authentication.getCredentials().toString();
    
    (2)     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"));
            }
        }
    
    

    (1)处从UsernamePasswordAuthenticationToken中调出了密码,再由(2)处通过调用成员变量passwordEncoder对其密码进行验证。

    相关文章

      网友评论

          本文标题:spring security AuthenticationM

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