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的驗證大致流程,由於本人能力有限,如有錯誤,還請各位大佬多多包涵並在評論區進行留言指正,我會一一回復。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM