Spring Security : 概念模型 ProviderManager 认证提供者管理器

ProviderManagerSpring Security提供的AuthenticationManager实现。其主要目的,也就是实现AuthenticationManager接口所定义的方法

Authentication authenticate(Authentication authentication) throws AuthenticationException

ProviderManager 使用一组AuthenticationProvider,也可以再附加一个双亲认证管理器AuthenticationManager来完成对一个认证请求,也就是一个认证令牌对象authentication的认证。

ProviderManager的认证过程也会发布相应的认证成功/异常事件。

认证过程概述

ProviderManager的认证逻辑会遍历所有支持该认证令牌对象参数 authentication (基于类型进行匹配)的 AuthenticationProvider,找到第一个能成功认证的并返回填充更多信息的authentication 对象:

  1. 如果某个 AuthenticationProvider 宣称可以认证该 authentication,但是认证过程抛出异常 AuthenticationException,则整个认证过程不会停止, 而是尝试使用下一个 AuthenticationProvider 继续;
  2. 如果某个 AuthenticationProvider 宣称可以认证该 authentication,但是认证过程抛出异常 AccountStatusException/InternalAuthenticationServiceException, 则异常会被继续抛出,整个认证过程停止;
  3. 如果某个 AuthenticationProvider 宣称可以认证该 authentication,并且成功认证该 authentication,则认证过程停止,该结果会被采用。

如果所有的 AuthenticationProvider 尝试完之后也未能认证该 authentication,并且双亲认证管理器被设置,则该方法会继续尝试使用双亲认证管理器认证该 authentication

如果所有的 AuthenticationProvider 都尝试过,并且双亲认证管理器也未能认证该 authentication,则会抛出异常 ProviderNotFoundException

认证成功时,如果设置了标志需要擦除认证中的凭证信息,则该方法会擦除认证中的凭证信息。

认证成功时,该方法也会调用 eventPublisher 发布认证成功事件。

认证异常时,该方法回调用 eventPublisher 发布相应的认证异常事件。

源代码

源代码版本 : Spring Security Config 5.1.4.RELEASE

package org.springframework.security.authentication;

import java.util.Collections;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;

/**
 * Iterates an Authentication request through a list of AuthenticationProviders.
 *
 * 
 * AuthenticationProviders are usually tried in order until one provides a
 * non-null response. A non-null response indicates the provider had authority to decide
 * on the authentication request and no further providers are tried. If a subsequent
 * provider successfully authenticates the request, the earlier authentication exception
 * is disregarded and the successful authentication will be used. If no subsequent
 * provider provides a non-null response, or a new AuthenticationException,
 * the last AuthenticationException received will be used. If no provider
 * returns a non-null response, or indicates it can even process an
 * Authentication, the ProviderManager will throw a
 * ProviderNotFoundException. A parent AuthenticationManager can also
 * be set, and this will also be tried if none of the configured providers can perform the
 * authentication. This is intended to support namespace configuration options though and
 * is not a feature that should normally be required.
 * 
 * The exception to this process is when a provider throws an
 * AccountStatusException, in which case no further providers in the list will be
 * queried.
 *
 * Post-authentication, the credentials will be cleared from the returned
 * Authentication object, if it implements the CredentialsContainer
 * interface. This behaviour can be controlled by modifying the
 * #setEraseCredentialsAfterAuthentication(boolean)
 * eraseCredentialsAfterAuthentication property.
 *
 * Event Publishing
 * 
 * Authentication event publishing is delegated to the configured
 * AuthenticationEventPublisher which defaults to a null implementation which
 * doesn't publish events, so if you are configuring the bean yourself you must inject a
 * publisher bean if you want to receive events. The standard implementation is
 * DefaultAuthenticationEventPublisher which maps common exceptions to events (in
 * the case of authentication failure) and publishes an
 * org.springframework.security.authentication.event.AuthenticationSuccessEvent
 * AuthenticationSuccessEvent} if authentication succeeds. If you are using the namespace
 * then an instance of this bean will be used automatically by the "http"
 * configuration, so you will receive events from the web part of your application
 * automatically.
 * 
 * Note that the implementation also publishes authentication failure events when it
 * obtains an authentication result (or an exception) from the "parent"
 * AuthenticationManager if one has been set. So in this situation, the parent
 * should not generally be configured to publish events or there will be duplicates.
 *
 *
 * @author Ben Alex
 * @author Luke Taylor
 *
 * @see DefaultAuthenticationEventPublisher
 */
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {
	// ~ Static fields/initializers
	// =======================================================

	private static final Log logger = LogFactory.getLog(ProviderManager.class);

	// ~ Instance fields
	// =======================================================

    // 认证事件发布器,这里缺省初始化为 NullEventPublisher,表示不做认证事件的发布
	private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
    // 用于记录所要使用的各个 AuthenticationProvider, 当前 ProviderManager 的认证
    // 任务最终委托给这组 AuthenticationProvider 完成
	private List<AuthenticationProvider> providers = Collections.emptyList();
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    // 双亲认证管理器,可以设置,也可以不设置,设置的话会在当前认证管理器 ProviderManager
    // 不能认证某个用户时再尝试使用该双亲认证管理器认证用户
	private AuthenticationManager parent;
    // 认证成功时是否擦除认证令牌对象中的凭证信息(比如密码),缺省值为 true
	private boolean eraseCredentialsAfterAuthentication = true;

    // 构造函数,指定一组要使用的 AuthenticationProvider,并且双亲认证管理器设置为 null
	public ProviderManager(List<AuthenticationProvider> providers) {
		this(providers, null);
	}

    // 构造函数,指定一组要使用的 AuthenticationProvider,并且双亲认证管理器设置为指定值
	public ProviderManager(List<AuthenticationProvider> providers,
			AuthenticationManager parent) {
		Assert.notNull(providers, "providers list cannot be null");
		this.providers = providers;
		this.parent = parent;
		checkState();
	}

	// ~ Methods
	// ======================================================

    // InitializingBean 接口定义的bean初始化方法,会在该bean创建过程中初始化阶段被调用,
    // 这里的实现仅仅检查必要的工作组件是否被设置,如果没有被设置,则抛出异常
    // IllegalArgumentException
	public void afterPropertiesSet() throws Exception {
		checkState();
	}

	private void checkState() {
		if (parent == null && providers.isEmpty()) {
			throw new IllegalArgumentException(
					"A parent AuthenticationManager or a list "
							+ "of AuthenticationProviders is required");
		}
	}

    // 尝试对认证请求对象,也就是认证令牌对象参数 authentication 进行认证
    // 该方法的逻辑会遍历所有支持该认证令牌对象参数 authentication (基于类型进行匹配)
    // 的 AuthenticationProvider,找到第一个能成功认证的并返回填充更多信息的
    // authentication 对象:
    // 1. 如果某个 AuthenticationProvider 宣称可以认证该 authentication,
    // 但是认证过程抛出异常 AuthenticationException,则整个认证过程不会停止,
    // 而是尝试使用下一个 AuthenticationProvider 继续;
    // 2. 如果某个 AuthenticationProvider 宣称可以认证该 authentication,
    // 但是认证过程抛出异常 AccountStatusException/InternalAuthenticationServiceException,
    // 则异常会被继续抛出,整个认证过程停止;
    // 3. 如果某个 AuthenticationProvider 宣称可以认证该 authentication,并且成功认证
    // 该 authentication,则认证过程停止,该结果会被采用。
    
    // 如果所有的 AuthenticationProvider 尝试完之后也未能认证该 authentication,
    // 并且双亲认证管理器被设置,则该方法会继续尝试使用双亲认证管理器认证该 authentication。
   
   // 如果所有的 AuthenticationProvider 都尝试过,并且双亲认证管理器也未能认证
   // 该 authentication,则会抛出异常  ProviderNotFoundException
   
    // 认证成功时,如果设置了标志需要擦除认证中的凭证信息,则该方法会擦除认证中的
    // 凭证信息。
    
    // 认证成功时,该方法也会调用 eventPublisher 发布认证成功事件。
   
   // 认证异常时,该方法回调用 eventPublisher 发布相应的认证异常事件。
   
	/**
	 * Attempts to authenticate the passed Authentication object.
	 * 
	 * The list of AuthenticationProviders will be successively tried until an
	 * AuthenticationProvider indicates it is capable of authenticating the
	 * type of Authentication object passed. Authentication will then be
	 * attempted with that AuthenticationProvider.
	 * 
	 * If more than one AuthenticationProvider supports the passed
	 * Authentication object, the first one able to successfully
	 * authenticate the Authentication object determines the
	 * result, overriding any possible AuthenticationException
	 * thrown by earlier supporting AuthenticationProviders.
	 * On successful authentication, no subsequent AuthenticationProviders
	 * will be tried.
	 * If authentication was not successful by any supporting
	 * AuthenticationProvider the last thrown
	 * AuthenticationException will be rethrown.
	 *
	 * @param authentication the authentication request object.
	 *
	 * @return a fully authenticated object including credentials.
	 *
	 * @throws AuthenticationException if authentication fails.
	 */
	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();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			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;
			}
			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;
	}

    // 发布认证异常事件
	@SuppressWarnings("deprecation")
	private void prepareException(AuthenticationException ex, Authentication auth) {
		eventPublisher.publishAuthenticationFailure(ex, auth);
	}

	/**
	 * Copies the authentication details from a source Authentication object to a
	 * destination one, provided the latter does not already have one set.
	 *
	 * @param source source authentication
	 * @param dest the destination authentication object
	 */
	private void copyDetails(Authentication source, Authentication dest) {
		if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
			AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;

			token.setDetails(source.getDetails());
		}
	}

	public List<AuthenticationProvider> getProviders() {
		return providers;
	}

	public void setMessageSource(MessageSource messageSource) {
		this.messages = new MessageSourceAccessor(messageSource);
	}

    // 指定事件发布器,用于覆盖缺省的 NullEventPublisher
	public void setAuthenticationEventPublisher(
			AuthenticationEventPublisher eventPublisher) {
		Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
		this.eventPublisher = eventPublisher;
	}

	/**
	 * If set to, a resulting Authentication which implements the
	 * CredentialsContainer interface will have its
	 * CredentialsContainer#eraseCredentials() eraseCredentials method called
	 * before it is returned from the authenticate() method.
	 *
	 * @param eraseSecretData set to false to retain the credentials data in
	 * memory. Defaults to true.
	 */
	public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
		this.eraseCredentialsAfterAuthentication = eraseSecretData;
	}

	public boolean isEraseCredentialsAfterAuthentication() {
		return eraseCredentialsAfterAuthentication;
	}

    // 这是一个缺省使用的认证事件发布器实现类,实际上并不发布任何认证事件,只是为了避免
    // ProviderManager 的属性 eventPublisher 为 null
	private static final class NullEventPublisher implements AuthenticationEventPublisher {
		public void publishAuthenticationFailure(AuthenticationException exception,
				Authentication authentication) {
		}

		public void publishAuthenticationSuccess(Authentication authentication) {
		}
	}
}

参考文章

Spring Security : 概念模型 AuthenticationManager 认证管理器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值