AuthenticationManager验证原理

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的验证大致流程,由于本人能力有限,如有错误,还请各位大佬多多包涵并在评论区进行留言指正,我会一一回复。

posted @ 2020-03-14 19:22  喜欢你的眉眼微笑i  阅读(10619)  评论(6编辑  收藏  举报