【長文剖析】Spring Cloud OAuth 生成Token 原始碼解析

冷冷gg發表於2019-04-15

內容較長,spring security oauth 整個放發過程的類都有詳細說明,建議大家儲存後 慢慢閱讀,或者當工具書查詢

Spring Security OAuth核心類圖解析

關於Oauth2是什麼以及Oauth2的四種授權模式請移步Oauth2官網

下面簡單介紹一下關於Spring Security OAuth基本的原理。這也是理解pig及其pigx的第一步。

下面這張圖涉及到了Spring OAuth的一些核心類和介面。

20189104922

上圖藍色的方塊代表執行過程中呼叫的具體的類,綠色的方塊代表整個執行流程中呼叫的類,綠色的括號中代表的是該介面呼叫的具體的實現類。

整個流程的入口點是在TokenEndpoint,由它來處理獲取令牌的請求,獲取令牌的請求預設是**/oauth/token**這個路徑。

  • 1.當TokenEndpoint收到請求時,它首先會呼叫ClientDetailsService,ClientDetaisService從名字上看就很可以知道是一個類似於UserDetailsService的介面,只不過UserDetailsService讀取的是使用者的資訊,而ClientDetailsService讀取的是第三方應用的資訊。

  • 2.登入請求頭中帶上Client的資訊,而這個類就可以做到根據ClientId讀取相應的配置資訊。而ClientDetailsSevice讀取到的資訊都會封裝到ClientDetails這個物件中。

  • 3.同時,TokenEndpoint還會建立一個TokenRequests的物件,這個物件中封裝了除了第三方應用以外的其他資訊。比如說grant_type,scope,username,password(限密碼模式)等等資訊,而這些資訊都是封裝在TokenRequests裡面的。同時,ClientDetails也會被放到TokenRequests中,因為第三方應用的資訊也是令牌請求的一部分。

  • 4.之後利用TokenRequests去呼叫一個叫做TokenGranter的令牌授權者的介面,這個介面其實是對四種不同的授權模式進行的一個封裝。在這個介面裡,它會根據請求傳遞過來的grant_type去挑一個具體的實現來執行令牌生成的邏輯。

  • 5.不論採用哪種方式進行令牌的生成,在這個生成的過程中都會產生兩個物件,一個是OAuth2Request,這個物件實際上是之前的ClientDetails和TokenRequests這兩個物件的一個整合。另一個Authorization封裝的實際上是當前授權使用者的一些資訊,也就是誰在進行授權行為,Authorization裡封裝的就是誰的資訊。這裡的使用者資訊是通過UserDetailsService進行讀取的。

  • 6.OAuth2Request和Authorization這兩個物件組合起來,會形成一個OAuth2Authorization物件,而這個最終產生的物件它的裡面就包含了當前是哪個第三方應用在請求哪個使用者以哪種授權模式(包括授權過程中的一些其他引數)進行授權,也就是這個物件會彙總之前的幾個物件的資訊都會封裝到OAuth2Authorization這個物件中。

  • 7.然後這個物件會傳遞到一個叫做AuthorizationServerTokenServices的介面的實現類,它拿到OAuth2Authorization中所有的資訊之後最終會生成一個OAuth2的令牌OAuth2AccessToken。

Spring Security OAuth的令牌生成過程

下面的是一個標準的POST請求並且在URL中攜帶引數的請求,但是這個請求不符合我們這邊測試的要求,原因看下面的注意事項。

 curl -H "Authorization:Basic dGVzdDp0ZXN0" -X POST http://localhost:8000/auth/oauth/token?username=admin&password=123456&grant_type=password&scope=server
複製程式碼

回車以後我們可以看到首先會經過閘道器的密碼解密過濾器,並且引數經過我們的一通改造之後已經可以獲取到正確的值了。

20189192927

經過上面的一通操作,我們已經拿到了獲取token的一些必要的請求了。clientId,clientSecret,grant_type,usename,password,scope,終於可以帶著我們的引數深入原始碼啦!

這裡結合上文提到的核心類圖來看效果更好

上文提過,OAuth2.0的認證的入口點位於TokenEndPoint。我們也可以看到,程式碼確實已經進來了。

201891105014

我們可以看到這個類上有一個@RequestMapping註解,它來處理/oauth/token的POST請求。

  1. 進來之後的第一步,就是在程式碼的95行,獲取請求頭中的clientId。
  2. 然後在96行呼叫getClientDetailsService().loadClientByClientId(clientId)方法獲取整個第三方應用的詳細配置。

201891105839

具體的引數的意義可以看spring-oauth-server 資料庫表說明

  1. 在拿到客戶端的資訊之後在程式碼的98行通過傳遞進來的引數和查詢出來的第三方應用資訊構建TokenRequest。

建立TokenRequest的程式碼很簡單,如下:

public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) {

    String clientId = requestParameters.get(OAuth2Utils.CLIENT_ID);
    if (clientId == null) {
        // if the clientId wasn't passed in in the map, we add pull it from the authenticated client object
        clientId = authenticatedClient.getClientId();
    }
    else {
        // otherwise, make sure that they match
        if (!clientId.equals(authenticatedClient.getClientId())) {
            throw new InvalidClientException("Given client ID does not match authenticated client");
        }
    }
    String grantType = requestParameters.get(OAuth2Utils.GRANT_TYPE);

    Set<String> scopes = extractScopes(requestParameters, clientId);
    TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);

    return tokenRequest;
}
複製程式碼

所以其實它就幹了一件事,校驗傳遞進來clientId和查詢出來的clientId,如果匹配的話,就根據之前傳遞進來的clientId和和查詢出來的第三方應用構建TokenRequest。

然後我們就拿到TokenRequest了,後面的程式碼很簡單了:

201891111934

無非就是對下面這些引數的校驗:

  • clientId:是否有值,值是否和查詢結果匹配
  • scope:請求的一些授權內容,所請求的授權必須是第三方應用可以傳送的授權集合的子集,否則無法通過校驗)
  • grant_type:必須顯式指定按照哪種授權模式獲取令牌
  • 判斷傳遞的授權模式是否是簡化模式,如果是簡化模式也會拋異常。因為簡化模式其實是對授權碼模式的一種簡化:在使用者的第一步的授權行為的時候就直接返回令牌,所以是不會有呼叫請求令牌服務的機會的
  • 判斷是不是授權碼模式,因為授權碼模式包含兩個步驟,在授權碼模式中發出的令牌中擁有的許可權不是由發令牌的請求決定的,而是在發令牌之前的授權的請求裡就已經決定好了。因此它會對請求過來的scope進行置空操作,然後根據之前發出去的授權碼裡的許可權重新設定你的scope,因此它根本不會使用請求令牌的這個請求中攜帶的scope引數。
  • 之後判斷是不是重新整理令牌的請求,應為重新整理令牌的請求有自己的scope,所以也會進行重新設定scope的操作。

經過一系列的校驗之後,最終TokenRequest會在132行傳遞給TokenGranter,然後由granter產生最終的accessToken。之後直接將accessToken寫入響應裡就可以了。

TokenGranter中總共封裝了四種授權模式加一個重新整理令牌的操作,我們看看其中的一些細節。

201891114811

CompositeTokenGranter中有一個集合,這個集合裡封裝著的就是五個會產生令牌的操作。

它會對遍歷這五種情況,並根據之前請求中攜帶的grant_type在五種情況中挑一種進行最終的accessToken的生成。

然後我們看這個程式碼的第38行的具體的grant方法。

201891115533

20189112212

首先在org.springframework.security.oauth2.provider.token.AbstractTokenGranter中判斷當前攜帶的授權型別和這個類所支援的授權型別是否匹配,如果不匹配就返回空值,如果匹配的話就進行令牌的生成操作。

59到第63行是重新獲取一下clientId和客戶端資訊跟授權型別再做一個校驗,67行的getAccessToken方法會產生最終的一個令牌。

這個方法也非常簡單:

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
    return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
複製程式碼

它實際上就是對tokenServices的一個呼叫,而tokenSerives其實就是從37行我們可以看到其實就是AuthorizationServerTokenServices。這個類要想建立accessToken需要一個OAuth2Authentication物件,所以createAccessToken中包含了一個方法getOAuth2Authentication

這個方法不同的授權模式會有不同的實現。

201891121611

Spring Security OAuth核心類圖解析中我們已經知道最終產生的Oauth2Authorization包含兩部分資訊,一部分是請求中的一些資訊,另一部分是根據請求獲取的授權使用者的資訊。而在不同的授權模式下獲取授權使用者的資訊的方式是不同的,比如說pigx所使用的密碼模式就是使用請求中攜帶的使用者名稱和密碼來獲取當前授權使用者中的授權資訊,而在授權碼模式的兩個步驟中是根據第一步發出授權碼的同時會記錄相關使用者的資訊,之後對第二步進行授權的時候根據第三方應用請求過來的授權碼再讀取該授權碼對應的使用者資訊。所以getOAuth2Authentication對於不同的授權型別有不同的實現。

我們以pigx所使用的密碼模式繼續下面的流程。密碼模式對應的是org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter

201891123119

而這個方法我們可以看到它其實就是根據所請求的使用者名稱和密碼去建立UsernamePasswordAuthenticationToken,然後傳遞給authenticationManager做認證,在這個認證過程中它會去呼叫com.pig4cloud.pigx.common.security.service.PigxUserDetailsServiceImplloadUserByUsername方法,根據使用者名稱和密碼去讀取使用者的資訊,之後我們其實就已經拿到Authorization的資訊,而Oauth2Request根據第85行我們可以知道是根據傳進來的第三方應用詳情和tokenRequest產生出來的,而86行的OAuth2Authentication也是由Oauth2RequestAuthorization這兩個物件拼接起來的。而拼接的方式就是呼叫 org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactorycreateOAuth2Request方法。

public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest) {
    return tokenRequest.createOAuth2Request(client);
}
複製程式碼

這個方法最終會建立一個由clientDetails和tokenRequest組合而成的OAuth2Request。

201891125353

拿到OAuth2Request就可以去生成OAuth2Authentication了。

OAuth2Authentication就是org.springframework.security.oauth2.provider.token.AbstractTokenGranter第71到73行最終傳遞進去生成accessToken的物件。

而OAuth2Authentications生成成功之後進行返回的話就可以執行AuthorizationServerTokenServicescreateAccessToken方法,而一旦這個access token生成成功並寫入響應進行返回那麼整個流程也就結束了,最終我們就拿到了想要的訪問令牌。

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
    return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
複製程式碼

具體建立accessToken的程式碼,我們需要仔細讀一讀org.springframework.security.oauth2.provider.token.DefaultTokenServicescreateAccessToken方法。

2018911396

首先這個類一進來就會嘗試在tokenStore中獲取accessToken,因為同一個使用者只要令牌沒過期那麼再次請求令牌的時候會把之前傳送的令牌再次發還。因此一開始就會找當前使用者已經存在的令牌。

如果已經傳送的令牌不為空,那麼會在87行判斷當前的令牌是否已經過期,如果令牌過期了,那麼就會在tokenStore裡把accessToken和refreshToken一起刪掉,如果令牌沒過期,那麼就把這個沒過期的令牌重新再存一下。因為可能使用者是使用另外的方式來訪問令牌的,比如說一開始用授權碼模式,後來用密碼模式,而這兩種模式需要存的資訊是不一樣的,所以這個令牌要重新store一次。之後直接返回這個不過期的令牌。

如果令牌已經過期了或者說這個是第一次請求,令牌壓根沒生成,就會走下面的邏輯。

201891132620

首先看看重新整理的令牌有沒有,如果重新整理的令牌沒有的話,那麼建立一枚重新整理的令牌。然後在121行根據authentication, refreshToken建立accessToken。而這個建立accessToken的方法也非常簡單:

private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
    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());

    return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
複製程式碼

OAuth2AccessToken其實就是用UUID建立一個accessToken,然後把過期時間,重新整理令牌和scope這些OAuth協議規定的必須要存在的引數設定上,設定完了以後它會判斷是否存在tokenEnhancer,如果存在tokenEnhancer它就會按照定製的tokenEnhancer增強生成出來的token。

拿到返回的令牌之後,在122行tokenStore會把拿到的令牌存起來,然後拿refreshToken存起來,最後把生成的令牌返回回去。

於是我們就獲取到了令牌。

201891133652

總結

【長文剖析】Spring Cloud OAuth 生成Token 原始碼解析

歡迎關注我們獲得更多的好玩JavaEE 實踐

相關文章