欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

史上最简单的Spring Security教程(二十七):AuthenticationManager默认实现之ProviderManager详解

程序员文章站 2022-06-02 23:23:51
...

 

Spring Security 框架中的另一个重要接口 AuthenticationManager, 被设计用于处理 Authentication 请求。

与 AuthenticationProvider 接口一致,AuthenticationManager 接口中有且只有一个方法,即authenticate(Authentication authentication) 方法。

Authentication authenticate(Authentication authentication)
      throws AuthenticationException;

该方法与 AuthenticationProvider 中的 authenticate 方法声明及功能完全一致,返回包含凭据的完整身份验证对象 authentication。但是,如果 AuthenticationProvider 不支持给定的 Authentication 的话,该方法可能会返回 null。在此情况下,下一个支持 authentication 的 AuthenticationProvider 将会被尝试。

AuthenticationManager 接口的默认实现为 ProviderManager,其逻辑也不复杂。

首先,便是调用 AuthenticationProvider 中的 supports(Class<?> authentication) 方法,判断是否支持当前的 Authentication 请求。

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
    ......
​
    for (AuthenticationProvider provider : getProviders()) {
      if (!provider.supports(toTest)) {
        continue;
      }

只有支持当前 Authentication 请求的 AuthenticationProvider 才会继续后续逻辑处理。

然后,便是调用 AuthenticationProvider 中的 authenticate(Authentication authentication) 方法进行身份认证。

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
    ......
​
    for (AuthenticationProvider provider : getProviders()) {
      if (!provider.supports(toTest)) {
        continue;
      }
​
      ......
​
      try {
        result = provider.authenticate(authentication);
        ......
      }

如果认证成功且返回的结果不为 null,则执行 authentication details 的拷贝逻辑

try {
    result = provider.authenticate(authentication);
​
    if (result != null) {
        copyDetails(authentication, result);
        break;
    }
}
​
......
​
private void copyDetails(Authentication source, Authentication dest) {
    if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
        AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
​
        token.setDetails(source.getDetails());
    }
}

如果发生 AccountStatusException 或 InternalAuthenticationServiceException 异常,则会通过Spring事件发布器AuthenticationEventPublisher 发布异常事件。

catch (AccountStatusException e) {
    prepareException(e, authentication);
    // SEC-546: Avoid polling additional providers if auth failure is due to
    // invalid account status
    throw e;
}
catch (InternalAuthenticationServiceException e) {
    prepareException(e, authentication);
    throw e;
}
​
......
​
private void prepareException(AuthenticationException ex, Authentication auth) {
    eventPublisher.publishAuthenticationFailure(ex, auth);
}

如果异常为其它类型的 AuthenticationException,则将此异常设置为 lastException 并返回。

catch (AuthenticationException e) {
    lastException = e;
}

如果认证结果为 null,且存在父 AuthenticationManager,则调用父 AuthenticationManager 进行同样的身份认证操作,其处理逻辑基本同上。

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

如果认证结果不为 null,同时,此时的 eraseCredentialsAfterAuthentication 参数为 true,且此时认证后的 Authentication 实现了 CredentialsContainer 接口,那么即调用 CredentialsContainer 接口的凭据擦除方法,即eraseCredentials,擦除相关凭据信息。

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

其中,有一个防止重复发布 AuthenticationSuccessEvent 事件的处理,即 parentResult 为空。如果 parentResult 为 null,则代表父 AuthenticationManager 不存在或者没有身份认证成功,也即没有发布过 AuthenticationSuccessEvent 事件。此时,便由此处发布 AuthenticationSuccessEvent 事件。

最后,便是对于 lastException 的相关处理。

如果 lastException 为 null,则代表当前的 Authentication 并没有对应支持的 Provider。此时,便会抛出相应异常。

if (lastException == null) {
    lastException = new ProviderNotFoundException(messages.getMessage(
        "ProviderManager.providerNotFound",
        new Object[] { toTest.getName() },
        "No AuthenticationProvider found for {0}"));
}

接下来,如同防止重复发布 AuthenticationSuccessEvent 事件的处理一样,也有一个防止 AbstractAuthenticationFailureEvent 事件重复发布的逻辑处理。如果 parentException 为 null,则代表父AuthenticationManager 不存在、没有进行身份认证或者发布过 AbstractAuthenticationFailureEvent 事件,此时,便由此处发布 AbstractAuthenticationFailureEvent 事件。

if (parentException == null) {
    prepareException(lastException, authentication);
}
​
throw lastException;

最后,抛出 lastException

但是,抛出 lastException 之后呢?其实,是被另外一个 Filter 捕获并初始化到当前用户的 Request 中,感兴趣的朋友可以关注后续的文章,会有详细的解释。

如同之前介绍其它重要接口一样,我们了解了其详细的逻辑以后,不妨自己自定义一个 AuthenticationManager 的实现。我们先不实现复杂的逻辑,就针对 UsernamePasswordAuthenticationToken 相对应的 DaoAuthenticationProvider 来实现一个 AuthenticationManager

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        ......
​
        if (provider.supports(toTest)) {
      ......
​
            try {
                result = provider.authenticate(authentication);
            }
       ......
            catch (AuthenticationException e) {
                lastException = e;
            }
​
            ......
​
        } else {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }
​
        ......
​
        throw lastException;
    }

此 AuthenticationManager 只接受 DaoAuthenticationProvider 或者其子类,可以单纯的作为例子来看,不作其它意义。

其它详细源码,请参考文末源码链接,可自行下载后阅读。

我是银河架构师,十年饮冰,难凉热血,愿历尽千帆,归来仍是少年! 

如果文章对您有帮助,请举起您的小手,轻轻【三连】,这将是笔者持续创作的动力源泉。当然,如果文章有错误,或者您有任何的意见或建议,请留言。感谢您的阅读!

 

源码

 

github

https://github.com/liuminglei/SpringSecurityLearning/tree/master/27

gitee

https://gitee.com/xbd521/SpringSecurityLearning/tree/master/27

 

史上最简单的Spring Security教程(二十七):AuthenticationManager默认实现之ProviderManager详解

 

相关标签: Web安全