首页 > 其他 > 详细

AuthenticationManager验证原理

时间:2020-03-14 20:42:26      阅读:216      评论:0      收藏:0      [点我收藏+]

AuthenticationManager相关类图

  • AuthenticationManager验证过程
    AuthenticationManager验证过程涉及到的类和接口较多,我们就从这里开始逐一分析,首先我手画了一张图作为索引,这张图说明了各个类和接口之间的关系。
    技术分享图片
  • AuthenticationManager 为认证管理接口类,其定义了认证方法authenticate()
  • ProviderManager 为验证管理类,实现了接口AuthenticationManager ,并在认证方法authenticate() 中将身份认证委托给具有认证资格的AuthenticationProvider 进行身份认证。
    技术分享图片

    从上图中我们可以看到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
    顾名思义,该接口用于修改用户的密码,只有在使用持久存储库时才有效,基于内存的方式会抛异常。(用处不大,只做了解即可)

流程分析

技术分享图片
技术分享图片

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认证的入口为AuthenticationManagerauthenticate()方法,从上面代码中我们可以看出,AuthenticationManagerDelegator使用了单例模式来防止AuthenticationManager在初始化时发生无限递归,因此我们只分析上方的两个实现类OAuth2AuthenticationManagerProviderManager

OAuth2AuthenticationManager

OAuth2AuthenticationManagerauthenticate()的方法代码如下:

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

ProviderManagerauthenticate()的方法代码如下:

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]中通过for循环拿到支持该类认证的AuthenticationProvider用于认证处理。
  • (2)处的代码,对用户进行身份认证,认证过程如下所示。

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

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对其密码进行验证。

以上就是AuthenticationManager的验证大致流程,由于本人能力有限,如有错误,还请各位大佬多多包涵并在评论区进行留言指正,我会一一回复。

AuthenticationManager验证原理

原文:https://www.cnblogs.com/agony-wxl/p/12487263.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!