Spring Security 学习02 — Basic Introduction Part II

Spring Security 学习02 — Basic Introduction Part II

Spring Security 5.1.4 RELEASE

在上一篇《Spring Security 学习01 — Basic Introduction Part I》中介绍了Spring Security的基本组件以及基本的认证流程,此篇介绍一下Spring Security的一些核心服务。

2 核心服务

Spring Security中还有许多十分重要的接口,特别是AuthenticationManager, UserDetailsServiceAccessDecisionManager,Spring Security提供了一些实现,用户亦可自己实现定制的认证授权机制。下面我们来具体看一下几个接口及Spring Security提供的实现,从而有助于了解在认证授权环节中它们的具体作用,以及如何使用。

2.1 AuthenticationManager, ProviderManager and AuthenticationProvider

AuthenticationManger是一个接口,用来完成认证的逻辑,开发者可按照自己的项目设计需求进行实现。如果,我们希望能够组合使用多种认证服务,比如基于数据库和LDAP服务器的认证服务,Spring Security也是支持的。

ProviderManager是Spring Security提供的一个实现,但是它本身不处理认证请求,而是将任务委托给一个配置好的AuthenticationProvider的列表,其中每一个AuthenticationProvider按序确认能否完成认证,每个provider如果认证失败,会抛出一个异常,如果认证通过,则会返回一个Authentication对象。

AuthenticationManager

AuthenticationManger是一个接口,其中只有一个方法authenticate,用来尝试对传入的Authentication对象进行认证。

这里保留了源码中的一大段注释,其中包括了该接口的作用,以及在实现时应当注意的一些问题,这里不多做叙述,我们目前只需要了解这个接口的作用即可,感兴趣的同学可以自行阅读源码中的注释。

/**
 * Processes an {@link Authentication} request.
 *
 * @author Ben Alex
 */
public interface AuthenticationManager {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Attempts to authenticate the passed {@link Authentication} object, returning a
	 * fully populated <code>Authentication</code> object (including granted authorities)
	 * if successful.
	 * <p>
	 * An <code>AuthenticationManager</code> must honour the following contract concerning
	 * exceptions:
	 * <ul>
	 * <li>A {@link DisabledException} must be thrown if an account is disabled and the
	 * <code>AuthenticationManager</code> can test for this state.</li>
	 * <li>A {@link LockedException} must be thrown if an account is locked and the
	 * <code>AuthenticationManager</code> can test for account locking.</li>
	 * <li>A {@link BadCredentialsException} must be thrown if incorrect credentials are
	 * presented. Whilst the above exceptions are optional, an
	 * <code>AuthenticationManager</code> must <B>always</B> test credentials.</li>
	 * </ul>
	 * Exceptions should be tested for and if applicable thrown in the order expressed
	 * above (i.e. if an account is disabled or locked, the authentication request is
	 * immediately rejected and the credentials testing process is not performed). This
	 * prevents credentials being tested against disabled or locked accounts.
	 *
	 * @param authentication the authentication request object
	 *
	 * @return a fully authenticated object including credentials
	 *
	 * @throws AuthenticationException if authentication fails
	 */
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}
ProviderManager

ProviderManagerAuthentication的一个实现,并将具体的认证操作委托给一系列的AuthenticationProvider来完成,从而可以实现支持多种认证方式。为了帮助阅读和理解源码具体做了什么,这里删除了原来的一部分注释,并对重要的部分进行了注释说明。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {

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

	private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
	private List<AuthenticationProvider> providers = Collections.emptyList();
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private AuthenticationManager parent;
	private boolean eraseCredentialsAfterAuthentication = true;

    /**
     * 用List<AuthenticationProvider>初始化一个ProviderManager
     * 也就是上文提到的,ProviderManager将具体的认证委托给不同的provider,从而支持不同的认证方式
     */      
	public ProviderManager(List<AuthenticationProvider> providers) {
		this(providers, null);
	}

    /**
     * 也可以为其设置一个父类
     */
	public ProviderManager(List<AuthenticationProvider> providers,
			AuthenticationManager parent) {
		Assert.notNull(providers, "providers list cannot be null");
		this.providers = providers;
		this.parent = parent;
		checkState();
	}

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

	/**
	 * ProviderManager的核心方法,authentication方法尝试对传入的Authentication对象进行认证,传入的Authentication是
	 * 以用户的提交的认证信息,比如用户名和密码,创建的一个Authentication对象。
	 * 
	 * 会依次询问各个AuthenticationProvider,当provider支持对传入的Authentication认证,
	 * 便会尝试使用该provider进行认证。如果有多个provider都支持认证传入的Authentication对象,
	 * 则只会使用第一个支持的provider进行认证。
	 *
	 * 一旦有一个provider认证成功了,便会忽略之前任何provider抛出的异常,之后的provider也不会再
	 * 继续认证的尝试。
	 *
	 * 如果所有provider都认证失败,方法则会抛出最后一个provider抛出的异常。
	 */
	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();

        // 依次使用各个provider尝试进行认证
		for (AuthenticationProvider provider : getProviders()) {
            // 如果provider不支持对传入的Authentication进行认证,则跳过。
			if (!provider.supports(toTest)) {
				continue;
			}

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

			try {
                // 调用provider的authenticate方法进行认证
				result = provider.authenticate(authentication);
				
                // 如果认证成功,则将authentication中用户的细节信息复制到result中
                // 然后跳出循环,不再尝试后面其他的provider
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				// 如果待认证的账号信息无误,但是账号本身异常,比如账号停用了,则抛出AccountStatusException异常,
				// 并通过prepareException方法,发布一个AbstractAuthenticationFailureEvent,避免继续尝试其他provider进行认证
                throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
                // 如果该provider认证失败,捕获异常AuthenticationException后不抛出,继续尝试下一个provider
                // lastException会记录下最后一个认证失败的provider抛出的AuthenticationException异常。
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// 如果所有provider都没能认证成功,则交给父类尝试认证
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// 父类如果抛出该异常不做处理,因为后面有对子类抛出该异常的处理
			}
			catch (AuthenticationException e) {
                // 父类也没能认证成功,则最后一个异常为来自父类认证失败的异常
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// 认证成功,从Authentication中删除密码秘钥等敏感信息
				((CredentialsContainer) result).eraseCredentials();
			}

			// 如果父类认证成功,则会发布一个AuthenticationSuccessEvent,
            // 这一步检查,防止子类重复发布
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
            
            //返回的result为一个Authentication,其中包含了已认证用户的信息
			return result;
            
		}

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

		// 如果父类认证失败,会发布一个AbstractAuthenticationFailureEvent
		// 这一步检查,防止子类重复发布
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}

	@SuppressWarnings("deprecation")
	private void prepareException(AuthenticationException ex, Authentication auth) {
		eventPublisher.publishAuthenticationFailure(ex, auth);
	}

	/**
	 * 从source中复制用户的信息到dest
	 *
	 * 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);
	}

	public void setAuthenticationEventPublisher(
			AuthenticationEventPublisher eventPublisher) {
		Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
		this.eventPublisher = eventPublisher;
	}

	/**
	 * 设置是否要在认证完成后,让Authentication调用自己的eraseCredentials方法来清除密码信息。
	 *
	 * If set to, a resulting {@code Authentication} which implements the
	 * {@code CredentialsContainer} interface will have its
	 * {@link CredentialsContainer#eraseCredentials() eraseCredentials} method called
	 * before it is returned from the {@code authenticate()} method.
	 *
	 * @param eraseSecretData set to {@literal false} to retain the credentials data in
	 * memory. Defaults to {@literal true}.
	 */
	public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
		this.eraseCredentialsAfterAuthentication = eraseSecretData;
	}

	public boolean isEraseCredentialsAfterAuthentication() {
		return eraseCredentialsAfterAuthentication;
	}

	private static final class NullEventPublisher implements AuthenticationEventPublisher {
		public void publishAuthenticationFailure(AuthenticationException exception,
				Authentication authentication) {
		}

		public void publishAuthenticationSuccess(Authentication authentication) {
		}
	}
}

至此可以看到,ProviderManager的认证逻辑还是很简单清晰的,我们也可以比较清楚理解AuthenticationManagerProviderManagerAuthenticationProvider的关系了。

AuthenticationProvider

AuthenticationProvider也是一个接口,用来完成具体的认证逻辑。不同的认证方式有不同的实现,Spring Security中提供了多种实现,包括DaoAuthenticationProvider,AnonymousAuthenticationProviderLdapAuthenticationProvider等。其中最简单的DaoAuthenticationProvider会在后面介绍,首先来看一下AuthenticationProvider的源码。同样的,保留了源码中的注释,感兴趣的同学可以细读,这里只做简单的介绍。

可以看到,AuthenticationProvider中只有2个方法:

  • authenticate完成具体的认证逻辑,如果认证失败,抛出AuthenticationException异常
  • supports判断是否支持传入的Authentication认证信息
/**
 * Indicates a class can process a specific
 * {@link org.springframework.security.core.Authentication} implementation.
 *
 * @author Ben Alex
 */
public interface AuthenticationProvider {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Performs authentication with the same contract as
	 * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
	 * .
	 *
	 * @param authentication the authentication request object.
	 *
	 * @return a fully authenticated object including credentials. May return
	 * <code>null</code> if the <code>AuthenticationProvider</code> is unable to support
	 * authentication of the passed <code>Authentication</code> object. In such a case,
	 * the next <code>AuthenticationProvider</code> that supports the presented
	 * <code>Authentication</code> class will be tried.
	 *
	 * @throws AuthenticationException if authentication fails.
	 */
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	/**
	 * Returns <code>true</code> if this <Code>AuthenticationProvider</code> supports the
	 * indicated <Code>Authentication</code> object.
	 * <p>
	 * Returning <code>true</code> does not guarantee an
	 * <code>AuthenticationProvider</code> will be able to authenticate the presented
	 * instance of the <code>Authentication</code> class. It simply indicates it can
	 * support closer evaluation of it. An <code>AuthenticationProvider</code> can still
	 * return <code>null</code> from the {@link #authenticate(Authentication)} method to
	 * indicate another <code>AuthenticationProvider</code> should be tried.
	 * </p>
	 * <p>
	 * Selection of an <code>AuthenticationProvider</code> capable of performing
	 * authentication is conducted at runtime the <code>ProviderManager</code>.
	 * </p>
	 *
	 * @param authentication
	 *
	 * @return <code>true</code> if the implementation can more closely evaluate the
	 * <code>Authentication</code> class presented
	 */
	boolean supports(Class<?> authentication);
}
DaoAuthenticationProvider

DaoAuthenticationProvider是Spring Security提供的最简单的一个AuthenticationProvider的实现,也是框架中最早支持的。它使用UserDetailsService作为一个DAO来查询用户名、密码以及用户的权限GrantedAuthority。它认证用户的方式就是简单的比较UsernamePasswordAuthenticationToken中由用户提交的密码和通过UserDetailsService查询获得的密码是否一致。

下面我们来看一下DaoAuthenticationProvider的源码,对于源码的说明也写在了注释中。

DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider,而后者实现了AuthenticationProvider接口。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	/**
	 * The plaintext password used to perform
	 * PasswordEncoder#matches(CharSequence, String)}  on when the user is
	 * not found to avoid SEC-2056.
	 */
	private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

	private PasswordEncoder passwordEncoder;

	/**
	 * The password used to perform
	 * {@link PasswordEncoder#matches(CharSequence, String)} on when the user is
	 * not found to avoid SEC-2056. This is necessary, because some
	 * {@link PasswordEncoder} implementations will short circuit if the password is not
	 * in a valid format.
	 */
	private volatile String userNotFoundEncodedPassword;

	private UserDetailsService userDetailsService;

	private UserDetailsPasswordService userDetailsPasswordService;

	public DaoAuthenticationProvider() {
		setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
	}

	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
        
        // 用户未提交密码,抛出异常BadCredentialsException
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
		
        // 从传入了Authentication对象中获取用户提交的密码
		String presentedPassword = authentication.getCredentials().toString();
		
        // 用passwordEncoder的matches方法,比较用户提交的密码和userDetails中查询到的正确密码。
        // 由于用户密码的存放一般都是hash后保密的,因此userDetails获取到的密码一般是一个hash值,而用户提交
        // 的是一个明文密码,因此需要对用户提交的密码进行同样的hash计算后再进行比较。
		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"));
		}
	}

	protected void doAfterPropertiesSet() throws Exception {
		Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
	}

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

	@Override
	protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		boolean upgradeEncoding = this.userDetailsPasswordService != null
				&& this.passwordEncoder.upgradeEncoding(user.getPassword());
		if (upgradeEncoding) {
			String presentedPassword = authentication.getCredentials().toString();
			String newPassword = this.passwordEncoder.encode(presentedPassword);
			user = this.userDetailsPasswordService.updatePassword(user, newPassword);
		}
		return super.createSuccessAuthentication(principal, authentication, user);
	}

	private void prepareTimingAttackProtection() {
		if (this.userNotFoundEncodedPassword == null) {
			this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
		}
	}

	private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
		if (authentication.getCredentials() != null) {
			String presentedPassword = authentication.getCredentials().toString();
			this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
		}
	}

	/**
	 * Sets the PasswordEncoder instance to be used to encode and validate passwords. If
	 * not set, the password will be compared using {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
	 *
	 * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
	 * types.
	 */
	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
		this.passwordEncoder = passwordEncoder;
		this.userNotFoundEncodedPassword = null;
	}

	protected PasswordEncoder getPasswordEncoder() {
		return passwordEncoder;
	}

	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	protected UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

	public void setUserDetailsPasswordService(
			UserDetailsPasswordService userDetailsPasswordService) {
		this.userDetailsPasswordService = userDetailsPasswordService;
	}
}
AbstractUserDetailsAuthenticationProvider

可以看到DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider, 而一个provider最核心的authenticate方法,便写在了AbstractUserDetailsAuthenticationProvider中,下面我们只关注一下authenticate这个方法的源码。

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// 从传入的Authentication对象中获取用户名
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
		
        // 根据用户名,从缓存中获取用户的UserDetails
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		
		if (user == null) {
			cacheWasUsed = false;
			// 如果从缓存中没有获取到用户,则通过方法retrieveUser来获取用户信息
            // retrieve方法为一个抽象方法,不同的子类中有不同的实现,而在子类中,一般又会通过UserDetailService来获取用户信息,返回UserDetails
			try {
				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);
            // additionalAuthenticationChecks为具体的认证逻辑,是一个抽象方法,在子类中实现。
            // 比如前文中DaoAuthenticationProvider中,便是比较用户提交的密码和UserDetails中的密码
			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();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

可以看到在DaoAuthenticationProvider中还用到UserDetailsService来查询用户的密码权限信息,并包装为UserDetails返回,然后与用户提交的用户名密码信息进行比较来完成认证。UserDetailsServiceUserDetails在不同的provider中都会被用到,关于这两个接口的说明,在下一篇文章中介绍。

总结

最后我们总结一下这几个接口和类

名称类型说明
AuthenticationManager接口完成认证
ProviderManager实现AuthenticationManager接口,将认证任务委托给不同的AuthenticationProvider
AuthenticationProvider接口具体认证逻辑的接口
AbstractUserDetailsAuthenticationProvider抽象类实现了AuthenticationProvider接口,获取用户信息并完成认证的抽象类
DaoAuthenticationProvider继承AbstractUserDetailsAuthenticationProvider,实现具体的认证逻辑

这些类和接口之间的关系,大致可以用下图进行表示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值