【SpringSecurity】 ProviderManager

Metadata

title: 【SpringSecurity】 ProviderManager
date: 2023-02-01 19:39
tags:
  - 行动阶段/完成
  - 主题场景/组件
  - 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
  - 细化主题/Module/SpringSecurity
categories:
  - SpringSecurity
keywords:
  - SpringSecurity
description: 【SpringSecurity】 ProviderManager

【SpringSecurity】 ProviderManager

ProviderManager 实现了 [[【Spring Security】 AuthenticationManager|AuthenticationManager]] 接口, 重写了 authenticate 方法。

@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取传入的Authentication 类型
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        int size = this.providers.size();
        // 循环AuthenticationProvider
        for (AuthenticationProvider provider : getProviders()) {
            // 判断是否支持当前认证方式
            if (!provider.supports(toTest)) {
                continue;
            }
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
                        provider.getClass().getSimpleName(), ++currentPosition, size));
            }
            try {
                // 调用AuthenticationProvider进行认证
                result = provider.authenticate(authentication);
                if (result != null) {
                    // 执行 authentication details 的拷贝逻辑
                    copyDetails(authentication, result);
                    break;
                }
            } catch (AccountStatusException | InternalAuthenticationServiceException ex) {
                // 如果发生 AccountStatusException 或 InternalAuthenticationServiceException 异常,则会通过Spring事件发布器AuthenticationEventPublisher 发布异常事件。
                prepareException(ex, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw ex;
            } catch (AuthenticationException ex) {
                lastException = ex;
            }
        }
        if (result == null && this.parent != null) {
            // Allow the parent to try.
            try {
                parentResult = this.parent.authenticate(authentication);
                result = parentResult;
            } catch (ProviderNotFoundException ex) {
                // 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 ex) {
                parentException = ex;
                lastException = ex;
            }
        }
        if (result != null) {
            if (this.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 then it
            // will publish an AuthenticationSuccessEvent
            // This check prevents a duplicate AuthenticationSuccessEvent if the parent
            // AuthenticationManager already published it
            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).
        if (lastException == null) {
            lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
                    new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
        }
        // If the parent AuthenticationManager was attempted and failed then 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;
    }

在 ProviderManager 的 authenticate 方法中,轮训成员变量 List providers。该 providers 中如果有一个 AuthenticationProvider 的 supports 函数返回 true,那么就会调用该 AuthenticationProvider 的 authenticate 函数认证,如果认证成功则整个认证过程结束。如果不成功,则继续使用下一个合适的 AuthenticationProvider 进行认证,只要有一个认证成功则为认证成功。

实际上,每个 AuthenticationProvider 知道如何执行特定类型的身份验证。例如,一个 AuthenticationProvider 可能能够验证用户名 / 密码,而另一个人可能能够验证 SAML 。这允许每个 AuthenticationProvider 人进行非常特定类型的身份验证,同时支持多种类型的身份验证并且只公开一个 AuthenticationManagerbean。

ProviderManager 还允许配置一个可选的父级 AuthenticationManager,在 AuthenticationProvider 无法执行身份验证的情况下咨询该父级。父项可以是的任何类型 AuthenticationManager,但通常是 ProviderManager。

事实上,多个 ProviderManager 实例可能共享同一个 parent AuthenticationManager。这在多个 SecurityFilterChain 实例具有共同身份验证(共享父级 AuthenticationManager)但也具有不同身份验证机制(不同 ProviderManager 实例)的情况下有些常见。

默认情况下,ProviderManager 将尝试从 Authentication 成功的身份验证请求返回的对象中清除任何敏感凭据信息。这可以防止密码等信息在 HttpSession。

当您使用用户对象的缓存时,这可能会导致问题,例如,提高无状态应用程序的性能。如果 Authentication 包含对缓存中对象(例如 UserDetails 实例)的引用,并且其凭据已删除,则将无法再针对缓存值进行身份验证。如果您使用缓存,则需要考虑到这一点。一个明显的解决方案是首先制作对象的副本,无论是在缓存实现中还是在 AuthenticationProvider 创建返回的 Authentication 对象中。或者,您可以禁用 CredentialsAfterAuthentication 属性 ProviderManager。