Spring Security 解析(七) —— Spring Security Oauth2 源碼解析


Spring Security 解析(七) —— Spring Security Oauth2 源碼解析

  在學習Spring Cloud 時,遇到了授權服務oauth 相關內容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth2 等權限、認證相關的內容、原理及設計學習並整理一遍。本系列文章就是在學習的過程中加強印象和理解所撰寫的,如有侵權請告知。

項目環境:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

  在解析Spring Security Oauth2 源碼前,我們先看下 Spring Security Oauth2 官方文檔 ,其中有這么一段描述:

The provider role in OAuth 2.0 is actually split between Authorization Service and Resource Service, and while these sometimes reside in the same application, with Spring Security OAuth you have the option to split them across two applications, and also to have multiple Resource Services that share an Authorization Service. The requests for the tokens are handled by Spring MVC controller endpoints, and access to protected resources is handled by standard Spring Security request filters. The following endpoints are required in the Spring Security filter chain in order to implement OAuth 2.0 Authorization Server:

  • AuthorizationEndpoint is used to service requests for authorization. Default URL: /oauth/authorize.
  • TokenEndpoint is used to service requests for access tokens. Default URL: /oauth/token.

The following filter is required to implement an OAuth 2.0 Resource Server:

  • The OAuth2AuthenticationProcessingFilter is used to load the Authentication for the request given an authenticated access token.

  翻譯后:

  實現OAuth 2.0授權服務器,Spring Security過濾器鏈中需要以下端點:

  • AuthorizationEndpoint 用於服務於授權請求。預設地址:/oauth/authorize。
  • TokenEndpoint 用於服務訪問令牌的請求。預設地址:/oauth/token。

  實現OAuth 2.0資源服務器,需要以下過濾器:

  • OAuth2AuthenticationProcessingFilter 用於加載給定的認證訪問令牌請求的認證。

  按照官方提示,我們開始源碼解析。(個人建議: 在看源碼前最好先去看下官方文檔,能夠減少不必要的時間)

一、 @EnableAuthorizationServer 解析

  我們都知道 一個授權認證服務器最最核心的就是 @EnableAuthorizationServer , 那么 @EnableAuthorizationServer 主要做了什么呢? 我們看下 @EnableAuthorizationServer 源碼:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {

}

   我們可以看到其源碼內部導入了 AuthorizationServerEndpointsConfigurationAuthorizationServerSecurityConfiguration 這2個配置類。 接下來我們分別看下這2個配置類具體做了什么。

(一)、 AuthorizationServerEndpointsConfiguration

   從這個配置類的名稱我們不難想象其內部肯定存在官方文檔中介紹的 AuthorizationEndpointTokenEndpoint ,那么我們通過源碼來印證下吧:

@Configuration
@Import(TokenKeyEndpointRegistrar.class)
public class AuthorizationServerEndpointsConfiguration {

  // 省略 其他相關配置代碼
  ....
  
  // 1、 AuthorizationEndpoint 創建
	@Bean
	public AuthorizationEndpoint authorizationEndpoint() throws Exception {
		AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint();
		FrameworkEndpointHandlerMapping mapping = getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
		authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
		authorizationEndpoint.setProviderExceptionHandler(exceptionTranslator());
		authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
		authorizationEndpoint.setTokenGranter(tokenGranter());
		authorizationEndpoint.setClientDetailsService(clientDetailsService);
		authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices());
		authorizationEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
		authorizationEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
		authorizationEndpoint.setUserApprovalHandler(userApprovalHandler());
		authorizationEndpoint.setRedirectResolver(redirectResolver());
		return authorizationEndpoint;
	}

  // 2、 TokenEndpoint 創建
	@Bean
	public TokenEndpoint tokenEndpoint() throws Exception {
		TokenEndpoint tokenEndpoint = new TokenEndpoint();
		tokenEndpoint.setClientDetailsService(clientDetailsService);
		tokenEndpoint.setProviderExceptionHandler(exceptionTranslator());
		tokenEndpoint.setTokenGranter(tokenGranter());
		tokenEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
		tokenEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
		tokenEndpoint.setAllowedRequestMethods(allowedTokenEndpointRequestMethods());
		return tokenEndpoint;
	}
	
	// 省略 其他相關配置代碼
	....

   通過源碼我們可以很明確的知道:

  • AuthorizationEndpoint 用於服務於授權請求。預設地址:/oauth/authorize。
  • TokenEndpoint 用於服務訪問令牌的請求。預設地址:/oauth/token。

   這里就不先解析 AuthorizationEndpoint 和 TokenEndpoint 源碼了,在下面我會專門解析的。

(二)、 AuthorizationServerSecurityConfiguration

   AuthorizationServerSecurityConfiguration 由於配置相對復雜,這里就不再貼源碼了介紹了。但其中最主要的配置 ClientDetailsServiceClientDetailsUserDetailsService 以及 ClientCredentialsTokenEndpointFilter 還是得講一講。
   這里介紹下 ClientDetailsUserDetailsService 、UserDetailsService、ClientDetailsService 3者之間的關系:

  • ClientDetailsService : 內部僅有 loadClientByClientId 方法。從方法名我們就可知其是通過 clientId 來獲取 Client 信息, 官方提供 JdbcClientDetailsService、InMemoryClientDetailsService 2個實現類,我們也可以像UserDetailsService 一樣編寫自己的實現類。
  • UserDetailsService : 內部僅有 loadUserByUsername 方法。這個類不用我再介紹了吧。不清楚得同學可以看下我之前得文章。
  • ClientDetailsUserDetailsService : UserDetailsService子類,內部維護了 ClientDetailsService 。其 loadUserByUsername 方法重寫后調用ClientDetailsService.loadClientByClientId()。

  ClientCredentialsTokenEndpointFilter 作用與 UserNamePasswordAuthenticationFilter 類似,通過攔截 /oauth/token 地址,獲取到 clientId 和 clientSecret 信息並創建 UsernamePasswordAuthenticationToken 作為 AuthenticationManager.authenticate() 參數 調用認證過程。整個認證過程唯一最大得區別在於 DaoAuthenticationProvider.retrieveUser() 獲取認證用戶信息時調用的是 ClientDetailsUserDetailsService,根據前面講述的其內部其實是調用ClientDetailsService 獲取到客戶端信息

二、 @EnableResourceServer 解析

  像授權認證服務器一樣,資源服務器也有一個最核心的配置 @EnableResourceServer , 那么 @EnableResourceServer 主要做了什么呢? 我們 一樣先看下 @EnableResourceServer 源碼:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ResourceServerConfiguration.class)
public @interface EnableResourceServer {

}

   從源碼中我們可以看到其導入了 ResourceServerConfiguration 配置類,這個配置類最核心的配置是 應用了 ResourceServerSecurityConfigurer ,我這邊貼出 ResourceServerSecurityConfigurer 源碼 最核心的配置代碼如下:



	@Override
	public void configure(HttpSecurity http) throws Exception {
     // 1、 創建 OAuth2AuthenticationManager  對象
		AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
		// 2、 創建 OAuth2AuthenticationProcessingFilter 過濾器
		resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
		resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
		resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
		if (eventPublisher != null) {
			resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
		}
		if (tokenExtractor != null) {
			resourcesServerFilter.setTokenExtractor(tokenExtractor);
		}
		if (authenticationDetailsSource != null) {
			resourcesServerFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
		}
		resourcesServerFilter = postProcess(resourcesServerFilter);
		resourcesServerFilter.setStateless(stateless);

		// @formatter:off
		http
			.authorizeRequests().expressionHandler(expressionHandler)
		.and()
			.addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class) // 3、 將 OAuth2AuthenticationProcessingFilter 過濾器加載到過濾器鏈上
			.exceptionHandling()
				.accessDeniedHandler(accessDeniedHandler)
				.authenticationEntryPoint(authenticationEntryPoint);
		// @formatter:on
	}
	
 private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
 	OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
 	if (authenticationManager != null) {
 		if (authenticationManager instanceof OAuth2AuthenticationManager) {
 			oauthAuthenticationManager = (OAuth2AuthenticationManager) authenticationManager;
 		}
 		else {
 			return authenticationManager;
 		}
 	}
 	oauthAuthenticationManager.setResourceId(resourceId);
 	oauthAuthenticationManager.setTokenServices(resourceTokenServices(http));
 	oauthAuthenticationManager.setClientDetailsService(clientDetails());
 	return oauthAuthenticationManager;
 }   	
	
	

   源碼中最核心的 就是 官方文檔中介紹的 OAuth2AuthenticationProcessingFilter 過濾器, 其配置分3步:

  • 1、 創建 OAuth2AuthenticationProcessingFilter 過濾器 對象
  • 2、 創建 OAuth2AuthenticationManager 對象 對將其作為參數設置到 OAuth2AuthenticationProcessingFilter 中
  • 3、 將 OAuth2AuthenticationProcessingFilter 過濾器添加到過濾器鏈上

三、 AuthorizationEndpoint 解析

   正如前面介紹一樣,AuthorizationEndpoint 本身 最大的功能點就是實現了 /oauth/authorize , 那么我們這次就來看看它是如何實現的:


 @RequestMapping(value = "/oauth/authorize")
 public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
 		SessionStatus sessionStatus, Principal principal) {

 	//  1、 通過 OAuth2RequestFactory 從 參數中獲取信息創建 AuthorizationRequest 授權請求對象
 	AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

 	Set<String> responseTypes = authorizationRequest.getResponseTypes();

 	if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
 		throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
 	}

 	if (authorizationRequest.getClientId() == null) {
 		throw new InvalidClientException("A client id must be provided");
 	}

 	try {
         // 2、 判斷  principal 是否 已授權 : /oauth/authorize 設置為無權限訪問 ,所以要判斷,如果 判斷失敗則拋出 InsufficientAuthenticationException (AuthenticationException 子類),其異常會被 ExceptionTranslationFilter 處理 ,最終跳轉到 登錄頁面,這也是為什么我們第一次去請求獲取 授權碼時會跳轉到登陸界面的原因
 		if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
 			throw new InsufficientAuthenticationException(
 					"User must be authenticated with Spring Security before authorization can be completed.");
 		}

         // 3、 通過 ClientDetailsService.loadClientByClientId() 獲取到 ClientDetails 客戶端信息
 		ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());

 		// 4、 獲取參數中的回調地址並且與系統配置的回調地址對比
 		String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
 		String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
 		if (!StringUtils.hasText(resolvedRedirect)) {
 			throw new RedirectMismatchException(
 					"A redirectUri must be either supplied or preconfigured in the ClientDetails");
 		}
 		authorizationRequest.setRedirectUri(resolvedRedirect);

 		//  5、 驗證 scope 
 		oauth2RequestValidator.validateScope(authorizationRequest, client);

 		//  6、 檢測該客戶端是否設置自動 授權(即 我們配置客戶端時配置的 autoApprove(true)  )
 		authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
 				(Authentication) principal);
 		boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
 		authorizationRequest.setApproved(approved);

 		if (authorizationRequest.isApproved()) {
 			if (responseTypes.contains("token")) {
 				return getImplicitGrantResponse(authorizationRequest);
 			}
 			if (responseTypes.contains("code")) {
 			    // 7 調用 getAuthorizationCodeResponse() 方法生成code碼並回調到設置的回調地址
 				return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
 						(Authentication) principal));
 			}
 		}
 		model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
 		model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));

 		return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

 	}
 	catch (RuntimeException e) {
 		sessionStatus.setComplete();
 		throw e;
 	}

 }


   我們來大致解析下這段邏輯:

  • 1、 通過 OAuth2RequestFactory 從 參數中獲取信息創建 AuthorizationRequest 授權請求對象
  • 2、 判斷 principal 是否 已授權 : /oauth/authorize 設置為無權限訪問 ,所以要判斷,如果 判斷失敗則拋出 InsufficientAuthenticationException (AuthenticationException 子類),其異常會被 ExceptionTranslationFilter 處理 ,最終跳轉到 登錄頁面,這也是為什么我們第一次去請求獲取 授權碼時會跳轉到登陸界面的原因
  • 3、 通過 ClientDetailsService.loadClientByClientId() 獲取到 ClientDetails 客戶端信息
  • 4、 獲取參數中的回調地址並且與系統配置的回調地址(步驟3獲取到的client信息)對比
  • 5、 與步驟4一樣 驗證 scope
  • 6、 檢測該客戶端是否設置自動 授權(即 我們配置客戶端時配置的 autoApprove(true))
  • 7、 由於我們設置 autoApprove(true) 則 調用 getAuthorizationCodeResponse() 方法生成code碼並回調到設置的回調地址
  • 8、 真實生成Code 的方法時 generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) 方法: 其內部是調用 authorizationCodeServices.createAuthorizationCode()方法生成code的

  生成授權碼的整個邏輯其實是相對簡單的,真正復雜的是token的生成邏輯,那么接下來我們就看看token的生成。

四、 TokenEndpoint 解析

   對於使用oauth2 的用戶來說,最最不可避免的就是token 的獲取,話不多說,源碼解析貼上:

 
 @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
 public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
 Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

     // 1、 驗證 用戶信息 (正常情況下會經過 ClientCredentialsTokenEndpointFilter 過濾器認證后獲取到用戶信息 )
 	if (!(principal instanceof Authentication)) {
 		throw new InsufficientAuthenticationException(
 				"There is no client authentication. Try adding an appropriate authentication filter.");
 	}

     // 2、 通過 ClientDetailsService().loadClientByClientId() 獲取系統配置客戶端信息
 	String clientId = getClientId(principal);
 	ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

     // 3、 通過客戶端信息生成 TokenRequest 對象
 	TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

     ......
     
     // 4、 調用 TokenGranter.grant()方法生成 OAuth2AccessToken 對象(即token)
 	OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
 	if (token == null) {
 		throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
 	}
     // 5、 返回token
 	return getResponse(token);

 }

   簡單概括下來,整個生成token 的邏輯如下:

  • 1、 驗證 用戶信息 (正常情況下會經過 ClientCredentialsTokenEndpointFilter 過濾器認證后獲取到用戶信息 )
  • 2、 通過 ClientDetailsService().loadClientByClientId() 獲取系統配置的客戶端信息
  • 3、 通過客戶端信息生成 TokenRequest 對象
  • 4、 將步驟3獲取到的 TokenRequest 作為TokenGranter.grant() 方法參照 生成 OAuth2AccessToken 對象(即token)
  • 5、 返回 token

   其中 步驟 4 是整個token生成的核心,我們來看下 TokenGranter.grant() 方法源碼:


 public class CompositeTokenGranter implements TokenGranter {
 
 	private final List<TokenGranter> tokenGranters;
 
 	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
 		for (TokenGranter granter : tokenGranters) {
 			OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
 			if (grant!=null) {
 				return grant;
 			}
 		}
 		return null;
 	}
 	
 	.....
 }

   官方默認調用 CompositeTokenGranter 的 grant()方法,從源碼中我們可以看到其聚合了 TokenGranter ,采用遍歷的方式一個一個的去嘗試,由於Oauth2 有4種模式外加token刷新,所以 官方目前有5個子類。
   Debug 看下 tokenGranters :
https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190925151551905-120915727.jpg
  從截圖中可以看出分別是: AuthorizationCodeTokenGranter、ClientCredentialsTokenGranter、ImplicitTokenGranter、RefreshTokenGranter、ResourceOwnerPasswordTokenGranter ,當然還有一個他們共同的 父類 AbstractTokenGranter。
其中除了 ClientCredentialsTokenGranter 重寫了 AbstractTokenGranter.grant() 方法以外,其他4中都是直接調用 AbstractTokenGranter.grant() 進行處理。 我們來看下 AbstractTokenGranter.grant() 其方法內部實現:


 public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

     // 1、 判斷 grantType 是否匹配
 	if (!this.grantType.equals(grantType)) {
 		return null;
 	}
 	
 	// 2、 獲取  ClientDetails 信息 並驗證 grantType 
 	String clientId = tokenRequest.getClientId();
 	ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
 	validateGrantType(grantType, client);

 	if (logger.isDebugEnabled()) {
 		logger.debug("Getting access token for: " + clientId);
 	}

     // 3、 調用 getAccessToken() 方法生成token並返回
 	return getAccessToken(client, tokenRequest);

 }

   AbstractTokenGranter.grant() 方法內部邏輯分3步:

  • 1、 判斷 grantType 是否匹配
  • 2、 獲取 ClientDetails 信息 並驗證 grantType
  • 3、 調用 getAccessToken() 方法生成token並返回

   到目前 我們還沒有看到token具體生成的邏輯,那么接下來我們就來揭開這層面紗:

 protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
 	return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
 }

   這里分2個步驟:

  • 1、 通過 getOAuth2Authentication() 方法(子類重寫)獲取到 OAuth2Authentication 對象
  • 2、 將步驟1 獲取到的 OAuth2Authentication 作為 tokenServices.createAccessToken() 方法入參生成token

   由於授權碼模式最為復雜,那么我們就以為例,查看 其 getOAuth2Authentication() 源碼:


 @Override
 protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
     
     // 1、 從TokenRequest 中 獲取 code 碼 、 回調url
 	Map<String, String> parameters = tokenRequest.getRequestParameters();
 	String authorizationCode = parameters.get("code");
 	String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI);

 	if (authorizationCode == null) {
 		throw new InvalidRequestException("An authorization code must be supplied.");
 	}
     // 2、 調用 authorizationCodeServices.consumeAuthorizationCode(authorizationCode) 方法通過 Code碼 獲取 OAuth2Authentication 對象
 	OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);
 	if (storedAuth == null) {
 		throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
 	}
     // 3、 從 OAuth2Authentication 對象中獲取 OAuth2Request 對象並驗證回調url、clientId
 	OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request();
 	String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(
 			OAuth2Utils.REDIRECT_URI);

 	if ((redirectUri != null || redirectUriApprovalParameter != null)
 			&& !pendingOAuth2Request.getRedirectUri().equals(redirectUri)) {
 		throw new RedirectMismatchException("Redirect URI mismatch.");
 	}

 	String pendingClientId = pendingOAuth2Request.getClientId();
 	String clientId = tokenRequest.getClientId();
 	if (clientId != null && !clientId.equals(pendingClientId)) {
 		throw new InvalidClientException("Client ID mismatch");
 	}
     // 4、 創建一個全新的 OAuth2Request,並從OAuth2Authentication 中獲取到 Authentication 對象
 	Map<String, String> combinedParameters = new HashMap<String, String>(pendingOAuth2Request
 			.getRequestParameters());
 	combinedParameters.putAll(parameters);
 	OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters);
 	
 	Authentication userAuth = storedAuth.getUserAuthentication();
 	
 	// 5、 創建一個全新的 OAuth2Authentication 對象
 	return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);

 }
 

   我們從源碼中可以看到,整個 getOAuth2Authentication 分5個步驟:

  • 1、 從TokenRequest 中 獲取 code 碼 、 回調url
  • 2、 調用 authorizationCodeServices.consumeAuthorizationCode(authorizationCode) 方法通過 Code碼 獲取 OAuth2Authentication 對象
  • 3、 從 OAuth2Authentication 對象中獲取 OAuth2Request 對象並驗證回調url、clientId
  • 4、 創建一個全新的 OAuth2Request,並從OAuth2Authentication 中獲取到 Authentication 對象
  • 5、 通過步驟4 的OAuth2Request 和 Authentication 創建一個全新的 OAuth2Authentication 對象

   這里可能有人會問怎么不直接使用原本通過code 獲取的 OAuth2Authentication 對象,這里我也不清楚,如果有同學清楚麻煩告知以下,謝謝!!

OAuth2Authentication 對象生成后會調用 tokenServices.createAccessToken(),我們來看下 官方默認提供 的 DefaultTokenServices(AuthorizationServerTokenServices 實現類) 的 createAccessToken 方法內部實現源碼:

 @Transactional
 public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
     // 1、 通過 tokenStore 獲取到之前存在的token 並判斷是否為空、過期,不為空且未過期則直接返回原有存在的token (由於我們常用Jwt 所以這里是 JwtTokenStore ,且 existingAccessToken 永遠為空,即每次請求獲取token的值均不同,這與RedisTokenStore 是有區別的)
 	OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
 	OAuth2RefreshToken refreshToken = null;
 	if (existingAccessToken != null) {
 		if (existingAccessToken.isExpired()) {
 			if (existingAccessToken.getRefreshToken() != null) {
 				refreshToken = existingAccessToken.getRefreshToken();
 				tokenStore.removeRefreshToken(refreshToken);
 			}
 			tokenStore.removeAccessToken(existingAccessToken);
 		}
 		else {
 			tokenStore.storeAccessToken(existingAccessToken, authentication);
 			return existingAccessToken;
 		}
 	}
     // 2、 調用 createRefreshToken 方法生成 refreshToken
 	if (refreshToken == null) {
 		refreshToken = createRefreshToken(authentication);
 	}else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
 		ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
 		if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
 			refreshToken = createRefreshToken(authentication);
 		}
 	}
     
     // 3、 調用  createAccessToken(authentication, refreshToken) 方法獲取 token
 	OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
 	tokenStore.storeAccessToken(accessToken, authentication);
 	// 4、 重新覆蓋原有的刷新token(原有的 refreshToken 為UUID 數據,覆蓋為 jwtToken)
 	refreshToken = accessToken.getRefreshToken();
 	if (refreshToken != null) {
 		tokenStore.storeRefreshToken(refreshToken, authentication);
 	}
 	return accessToken;

 }
 

   我們從源碼中可以看到,整個 createAccessToken 分4個步驟:

  • 1、 通過 tokenStore 獲取到之前存在的token 並判斷是否為空、過期,不為空且未過期則直接返回原有存在的token (由於我們常用Jwt 所以這里是 JwtTokenStore ,且 existingAccessToken 永遠為空,即每次請求獲取token的值均不同,這與RedisTokenStore 是有區別的)
  • 2、 調用 createRefreshToken 方法生成 refreshToken
  • 3、 調用 createAccessToken(authentication, refreshToken) 方法獲取 token
  • 4、 重新覆蓋原有的刷新token(原有的 refreshToken 為UUID 數據,覆蓋為 jwtToken)並返回token

   在現在為止我們還沒有看到token的生成代碼,不要灰心,立馬就能看到了 ,我們在看下步驟3 其 重載方法 createAccessToken(authentication, refreshToken) 源碼:

 private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
     // 1、 通過 UUID 創建  DefaultOAuth2AccessToken  並設置上有效時長等信息
 	DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
 	int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
 	if (validitySeconds > 0) {
 		token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
 	}
 	token.setRefreshToken(refreshToken);
 	token.setScope(authentication.getOAuth2Request().getScope());
     // 2、 判斷 是否存在 token增強器 accessTokenEnhancer ,存在則調用增強器增強方法
 	return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
 }

   從源碼來看,其實token就是通過UUID生成的,且生成過程很簡單,但 如果我們配置了token增強器 (TokenEnhancer)(對於jwtToken來說,其毋庸置疑的使用了增強器實現),所以我們還得看下增強器是如何實現的,不過在講解增強器的實現時,我們還得回顧下之前我們在TokenStoreConfig 配置過以下代碼:

     /**
      * 自定義token擴展鏈
      *
      * @return tokenEnhancerChain
      */
     @Bean
     public TokenEnhancerChain tokenEnhancerChain() {
         TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
         tokenEnhancerChain.setTokenEnhancers(Arrays.asList(new JwtTokenEnhance(), jwtAccessTokenConverter()));
         return tokenEnhancerChain;
     }

   這段代碼 配置了 tokenEnhancerChain (TokenEnhancer實現類),並且在 tokenEnhancerChain對象中添加了2個 TokenEnhance ,分別是 JwtAccessTokenConverter 以及一個我們自定義的 增強器 JwtTokenEnhance ,所以看到這里應該能夠明白 最終會調用 tokenEnhancerChain ,不用想,tokenEnhancerChain肯定會遍歷 其內部維護的 TokenEnhanceList進行token增強,查看 tokenEnhancerChain 源碼如下:

public class TokenEnhancerChain implements TokenEnhancer {

 private List<TokenEnhancer> delegates = Collections.emptyList();

 /**
  * @param delegates the delegates to set
  */
 public void setTokenEnhancers(List<TokenEnhancer> delegates) {
 	this.delegates = delegates;
 }

 /**
  * Loop over the {@link #setTokenEnhancers(List) delegates} passing the result into the next member of the chain.
  * 
  * @see org.springframework.security.oauth2.provider.token.TokenEnhancer#enhance(org.springframework.security.oauth2.common.OAuth2AccessToken,
  * org.springframework.security.oauth2.provider.OAuth2Authentication)
  */
 public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
 	OAuth2AccessToken result = accessToken;
 	for (TokenEnhancer enhancer : delegates) {
 		result = enhancer.enhance(result, authentication);
 	}
 	return result;
 }

}

   至於其增強器實現代碼這里就不再貼出了。至此,個人覺得整個獲取token的源碼解析基本上完成。如果非得要總結的話 請看下圖:

https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190925151556531-500611638.jpg

五、 OAuth2AuthenticationProcessingFilter (資源服務器認證)解析

  通過前面的解析我們最終獲取到了token,但獲取token 不是我們最終目的,我們最終的目的時拿到資源信息,所以我們還得通過獲取到的token去調用資源服務器接口獲取資源數據。那么接下來我們就來解析資源服務器是如何通過傳入token去辨別用戶並允許返回資源信息的。我們知道資源服務器在過濾器鏈新增了 OAuth2AuthenticationProcessingFilter 來攔截請求並認證,那就這個過濾器的實現:

 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
 		ServletException {

 	final boolean debug = logger.isDebugEnabled();
 	final HttpServletRequest request = (HttpServletRequest) req;
 	final HttpServletResponse response = (HttpServletResponse) res;

 	try {
         // 1、 調用 tokenExtractor.extract() 方法從請求中解析出token信息並存放到 authentication 的  principal 字段 中
 		Authentication authentication = tokenExtractor.extract(request);
 		
 		if (authentication == null) {
 			if (stateless && isAuthenticated()) {
 				if (debug) {
 					logger.debug("Clearing security context.");
 				}
 				SecurityContextHolder.clearContext();
 			}
 			if (debug) {
 				logger.debug("No token in request, will continue chain.");
 			}
 		}
 		else {
 			request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
 			if (authentication instanceof AbstractAuthenticationToken) {
 				AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
 				needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
 			}
 			// 2、 調用  authenticationManager.authenticate() 認證過程: 注意此時的  authenticationManager 是 OAuth2AuthenticationManager 
 			Authentication authResult = authenticationManager.authenticate(authentication);

 			if (debug) {
 				logger.debug("Authentication success: " + authResult);
 			}

 			eventPublisher.publishAuthenticationSuccess(authResult);
 			SecurityContextHolder.getContext().setAuthentication(authResult);

 		}
 	}
 	catch (OAuth2Exception failed) {
 		SecurityContextHolder.clearContext();
 		eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
 				new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
 				
 		authenticationEntryPoint.commence(request, response,
 				new InsufficientAuthenticationException(failed.getMessage(), failed));

 		return;
 	}
     
 	chain.doFilter(request, response);
 }

   整個filter步驟最核心的是下面2個:

  • 1、 調用 tokenExtractor.extract() 方法從請求中解析出token信息並存放到 authentication 的 principal 字段 中
  • **2、 調用 authenticationManager.authenticate() 認證過程: 注意此時的 authenticationManager 是 OAuth2AuthenticationManager **

   在解析@EnableResourceServer 時我們講過 OAuth2AuthenticationManager 與 OAuth2AuthenticationProcessingFilter 的關系,這里不再重述,我們直接看下 OAuth2AuthenticationManager 的 authenticate() 方法實現:

 public Authentication authenticate(Authentication authentication) throws AuthenticationException {

 	if (authentication == null) {
 		throw new InvalidTokenException("Invalid token (token not found)");
 	}
 	// 1、 從 authentication 中獲取 token
 	String token = (String) authentication.getPrincipal();
 	// 2、 調用 tokenServices.loadAuthentication() 方法  通過 token 參數獲取到 OAuth2Authentication 對象 ,這里的tokenServices 就是我們資源服務器配置的。
 	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());
 		}
 	}
 	// 4、 設置認證成功標識並返回
 	auth.setDetails(authentication.getDetails());
 	auth.setAuthenticated(true);
 	return auth;

 }
 

   整個 認證邏輯分4步:

  • 1、 從 authentication 中獲取 token
  • 2、 調用 tokenServices.loadAuthentication() 方法 通過 token 參數獲取到 OAuth2Authentication 對象 ,這里的tokenServices 就是我們資源服務器配置的。
  • 3、 檢測客戶端信息,由於我們采用授權服務器和資源服務器分離的設計,所以這個檢測方法實際沒有檢測
  • 4、 設置認證成功標識並返回 ,注意返回的是 OAuth2Authentication (Authentication 子類)。

   后面的授權過程就是原汁原味的Security授權,所以至此整個資源服務器 通過獲取到的token去調用接口獲取資源數據 的解析完成。

六、 重寫登陸,實現登錄接口直接返回jwtToken

   前面,我們花了大量時間講解,那么肯定得實踐實踐一把。 相信大家平時的登錄接口都是直接返回token的,但是由於Security 最原本的設計原因,登陸后都是跳轉回到之前求情的接口,這種方式僅僅適用於PC端,那如果是APP呢?所以我們想要在原有的登陸接口上實現當非PC請求時返回token的功能。還記得之前提到過的 AuthenticationSuccessHandler 認證成功處理器,我們的功能實現就在這里面。

   我們重新回顧下 /oauth/authorize 實現 token,模仿實現后的代碼如下:


@Component("customAuthenticationSuccessHandler")
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

 @Resource
 private SecurityProperties securityProperties;

 @Resource
 private ObjectMapper objectMapper;

 @Resource
 private PasswordEncoder passwordEncoder;

 private ClientDetailsService clientDetailsService = null;

 private AuthorizationServerTokenServices authorizationServerTokenServices = null;

 private RequestCache requestCache = new HttpSessionRequestCache();

 @Override
 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                     Authentication authentication) throws IOException, ServletException {
     logger.info("登錄成功");
     // 重構后使得成功處理器能夠根據不同的請求來區別是返回token還是調用原來的邏輯(比如授權模式就需要跳轉)
     // 獲取請求頭中的Authorization

     String header = request.getHeader("Authorization");
     // 是否以Basic開頭
     if (header == null || !header.startsWith("Basic ")) {
         // 為了授權碼模式 登陸正常跳轉,這里就不再跳轉到自定義的登陸成功頁面了
//            // 如果設置了loginSuccessUrl,總是跳到設置的地址上
//            // 如果沒設置,則嘗試跳轉到登錄之前訪問的地址上,如果登錄前訪問地址為空,則跳到網站根路徑上
//            if (!StringUtils.isEmpty(securityProperties.getLogin().getLoginSuccessUrl())) {
//                requestCache.removeRequest(request, response);
//                setAlwaysUseDefaultTargetUrl(true);
//                setDefaultTargetUrl(securityProperties.getLogin().getLoginSuccessUrl());
//            }
         super.onAuthenticationSuccess(request, response, authentication);
     } else {

         // 這里為什么要通過 SpringContextUtil 獲取bean,
         // 主要原因是如果直接在 依賴注入 會導致 AuthorizationServerConfiguration 和 SpringSecurityConfig 配置加載順序混亂
         // 最直接的表現在 AuthorizationServerConfiguration 中 authenticationManager 獲取到 為null,因為這個時候 SpringSecurityConfig 還沒加載創建
         // 這里采用這種方式會有一定的性能問題,但也是無賴之舉  有興趣的同學可以看下: https://blog.csdn.net/qq_36732557/article/details/80338570 和 https://blog.csdn.net/forezp/article/details/84313907
         if (clientDetailsService == null && authorizationServerTokenServices == null) {
             clientDetailsService = SpringContextUtil.getBean(ClientDetailsService.class);
             authorizationServerTokenServices = SpringContextUtil.getBean(AuthorizationServerTokenServices.class);
         }

         String[] tokens = extractAndDecodeHeader(header, request);
         assert tokens.length == 2;

         String clientId = tokens[0];

         String clientSecret = tokens[1];

         ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

         if (clientDetails == null) {
             throw new UnapprovedClientAuthenticationException("clientId對應的配置信息不存在:" + clientId);
         } else if (!passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
             throw new UnapprovedClientAuthenticationException("clientSecret不匹配:" + clientId);
         }

         TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP,
                 clientId,
                 clientDetails.getScope(),
                 "custom");

         OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

         OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request,
                 authentication);

         OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

         response.setContentType("application/json;charset=UTF-8");
         response.getWriter().write(objectMapper.writeValueAsString(token));
     }

 }

 /**
  * 解析請求頭拿到clientid  client secret的數組
  *
  * @param header
  * @param request
  * @return
  * @throws IOException
  */
 private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {

     byte[] base64Token = header.substring(6).getBytes("UTF-8");
     byte[] decoded;
     try {
         decoded = Base64.decode(base64Token);
     } catch (IllegalArgumentException e) {
         throw new BadCredentialsException("Failed to decode basic authentication token");
     }

     String token = new String(decoded, "UTF-8");

     int delim = token.indexOf(":");

     if (delim == -1) {
         throw new BadCredentialsException("Invalid basic authentication token");
     }
     return new String[]{token.substring(0, delim), token.substring(delim + 1)};
 }

}

   回顧下創建token 需要的 幾個必要類:** clientDetailsService 、 authorizationServerTokenServices、 ClientDetails 、 TokenRequest 、OAuth2Request、 authentication、OAuth2Authentication **。 了解這幾個類之間的關系很有必要。對於clientDetailsService 、 authorizationServerTokenServices 我們可以直接從Spring 容器中獲取,ClientDetails 我們可以從請求參數中獲取,有了 ClientDetails 就有了 TokenRequest,有了 TokenRequest 和 authentication(認證后肯定有的) 就有了 OAuth2Authentication ,有了OAuth2Authentication 就能夠生成 OAuth2AccessToken。
至此,我們通過直接請求登陸接口(注意在請求頭中添加ClientDetails信息)就可以實現獲取到token了,那么有同學會問,如果我是手機登陸方式呢?其實不管你什么登陸方式,只要你設置的登陸成功處理器是上面那個就可支持,下圖是我測試的手機登陸獲取token截圖:

https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190925151601185-297144635.jpg

curl:

 curl -X POST \
   'http://localhost/loginByMobile?mobile=15680659123&smsCode=215672' \
   -H 'Accept: */*' \
   -H 'Accept-Encoding: gzip, deflate' \
   -H 'Authorization: Basic Y2xpZW50MToxMjM0NTY=' \
   -H 'Cache-Control: no-cache' \
   -H 'Connection: keep-alive' \
   -H 'Content-Length: 0' \
   -H 'Content-Type: application/json' \
   -H 'Host: localhost' \
   -H 'Postman-Token: 412722f9-b303-4d5d-b4a4-72b1dcb47f44,572f537f-c2f7-4c9c-a0e9-5e0eb07a3ec5' \
   -H 'User-Agent: PostmanRuntime/7.17.1' \
   -H 'cache-control: no-cache'

   注意: 請求頭中添加ClientDetails信息

七、 個人總結

   個人覺得官方的這段描述是最好的總結:

實現OAuth 2.0授權服務器,Spring Security過濾器鏈中需要以下端點:

  • AuthorizationEndpoint 用於服務於授權請求。預設地址:/oauth/authorize。
  • TokenEndpoint 用於服務訪問令牌的請求。預設地址:/oauth/token。

  實現OAuth 2.0資源服務器,需要以下過濾器:

  • OAuth2AuthenticationProcessingFilter 用於加載給定的認證訪問令牌請求的認證。

   源碼解析的話,只要理解了下圖中所有涉及到的類的作用即出發場景就基本上算是明白了:

https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190925151556531-500611638.jpg

   本文介紹 Spring Security Oauth2 源碼解析 可以訪問代碼倉庫中的 security 模塊 ,項目的github 地址 : https://github.com/BUG9/spring-security

         如果您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!


免責聲明!

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



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