歡迎光臨
每天分享高質量文章

Re:從零開始的Spring Security OAuth2(二)

本文開始從原始碼的層面,講解一些Spring Security Oauth2的認證流程。本文較長,適合在空餘時間段觀看。且涉及了較多的原始碼,非關鍵性程式碼以…代替。


作者:老徐

原文地址:http://t.cn/RW9Pjps

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。


準備工作

首先開啟debug資訊:

logging:
 level:
   org.springframework: DEBUG

可以完整的看到內部的運轉流程。

client樣式稍微簡單一些,使用client樣式獲取token
http://localhost:8080/oauth/token?client_id=client_1&client;_secret=123456&scope;=select&grant;_type=client_credentials

由於debug資訊太多了,我簡單按照順序列了一下關鍵的幾個類:

ClientCredentialsTokenEndpointFilter
DaoAuthenticationProvider
TokenEndpoint
TokenGranter

@EnableAuthorizationServer

上一篇部落格中我們嘗試使用了password樣式和client樣式,有一個比較關鍵的endpoint:/oauth/token。從這個入口開始分析,spring security oauth2內部是如何生成token的。獲取token,與第一篇文章中的兩個重要概念之一有關,也就是AuthorizationServer與ResourceServer中的AuthorizationServer。

在之前的配置中

@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {}

出現了AuthorizationServerConfigurerAdapter 關鍵類,他關聯了三個重要的配置類,分別是

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
   @Override
   public void configure(AuthorizationServerSecurityConfigurer security <1>) throws Exception{
   }

   @Override
   public void configure(ClientDetailsServiceConfigurer clients <2>) throws Exception {
   }

   @Override
   public void configure(AuthorizationServerEndpointsConfigurer endpoints <3>) throws Exception {
   }

}

<1> 配置AuthorizationServer安全認證的相關資訊,建立ClientCredentialsTokenEndpointFilter核心過濾器

<2> 配置OAuth2的客戶端相關資訊

<3> 配置AuthorizationServerEndpointsConfigurer眾多相關類,包括配置身份認證器,配置認證方式,TokenStore,TokenGranter,OAuth2RequestFactory

我們逐步分析其中關鍵的類

客戶端身份認證核心過濾器ClientCredentialsTokenEndpointFilter(掌握)

擷取關鍵的程式碼,可以分析出大概的流程
在請求到達/oauth/token之前經過了ClientCredentialsTokenEndpointFilter這個過濾器,關鍵方法如下

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
       throws AuthenticationException, IOException, ServletException
{
   ...
   String clientId = request.getParameter("client_id");
   String clientSecret = request.getParameter("client_secret");

   ...
   clientId = clientId.trim();
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
           clientSecret);

   return this.getAuthenticationManager().authenticate(authRequest);

}

頂級身份管理者AuthenticationManager(掌握)

用來從請求中獲取client_id,client_secret,組裝成一個UsernamePasswordAuthenticationToken作為身份標識,使用容器中的頂級身份管理器AuthenticationManager去進行身份認證(AuthenticationManager的實現類一般是ProviderManager。而ProviderManager內部維護了一個List,真正的身份認證是由一系列AuthenticationProvider去完成。而AuthenticationProvider的常用實現類則是DaoAuthenticationProvider,DaoAuthenticationProvider內部又聚合了一個UserDetailsService介面,UserDetailsService才是獲取使用者詳細資訊的最終介面,而我們上一篇文章中在記憶體中配置使用者,就是使用了UserDetailsService的一個實現類InMemoryUserDetailsManager)。UML類圖可以大概理解下這些類的關係,省略了授權部分。

圖1 認證相關UML類圖

可能機智的讀者會發現一個問題,我前面一篇文章已經提到了client樣式是不存在“使用者”的概念的,那麼這裡的身份認證是在認證什麼呢?debug可以發現UserDetailsService的實現被適配成了ClientDetailsUserDetailsService,這個設計是將client客戶端的資訊(client_id,client_secret)適配成使用者的資訊(username,password),這樣我們的認證流程就不需要修改了。

經過ClientCredentialsTokenEndpointFilter之後,身份資訊已經得到了AuthenticationManager的驗證。接著便到達了
TokenEndpoint。

Token處理端點TokenEndpoint(掌握)

前面的兩個ClientCredentialsTokenEndpointFilter和AuthenticationManager可以理解為一些前置校驗,和身份封裝,而這個類一看名字就知道和我們的token是密切相關的。

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {

   @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
   public ResponseEntity postAccessToken(Principal principal, @RequestParam
   Map parameters)
throws HttpRequestMethodNotSupportedException
{
        ...
       String clientId = getClientId(principal);
       ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);//<1>
       ...
       TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);//<2>
       ...
       OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);//<3>
       ...
       return getResponse(token);

   }

   private TokenGranter tokenGranter;
}

<1> 載入客戶端資訊

<2> 結合請求資訊,建立TokenRequest

<3> 將TokenRequest傳遞給TokenGranter頒發token

省略了一些校驗程式碼之後,真正的/oauth/token端點暴露在了我們眼前,其中方法引數中的Principal經過之前的過濾器,已經被填充了相關的資訊,而方法的內部則是依賴了一個TokenGranter 來頒發token。其中OAuth2AccessToken的實現類DefaultOAuth2AccessToken就是最終在控制檯得到的token序列化之前的原始類:

public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken {
 private static final long serialVersionUID = 914967629530462926L;
 private String value;
 private Date expiration;
 private String tokenType = BEARER_TYPE.toLowerCase();
 private OAuth2RefreshToken refreshToken;
 private Set scope;
 private Map additionalInformation = Collections.emptyMap();
 //getter,setter
}
@org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class)
@org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2AccessTokenJackson1Deserializer.class)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2AccessTokenJackson2Serializer.class)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class)
public interface OAuth2AccessToken {
   public static String BEARER_TYPE = "Bearer";
   public static String OAUTH2_TYPE = "OAuth2";
   public static String ACCESS_TOKEN = "access_token";
   public static String TOKEN_TYPE = "token_type";
   public static String EXPIRES_IN = "expires_in";
   public static String REFRESH_TOKEN = "refresh_token";
   public static String SCOPE = "scope";
   ...
}

一個典型的樣例token響應,如下所示,就是上述類序列化後的結果:

{ 
   "access_token":"950a7cc9-5a8a-42c9-a693-40e817b1a4b0",
   "token_type":"bearer",
   "refresh_token":"773a0fcd-6023-45f8-8848-e141296cb3cb",
   "expires_in":27036,
   "scope":"select"
}

TokenGranter(掌握)

先從UML類圖對TokenGranter介面的設計有一個宏觀的認識

圖2 TokenGranter相關UML類圖

TokenGranter的設計思路是使用CompositeTokenGranter管理一個List串列,每一種grantType對應一個具體的真正授權者,在debug過程中可以發現CompositeTokenGranter 內部就是在迴圈呼叫五種TokenGranter實現類的grant方法,而granter內部則是透過grantType來區分是否是各自的授權型別。

public class CompositeTokenGranter implements TokenGranter {

   private final List tokenGranters;

   public CompositeTokenGranter(List tokenGranters) {
       this.tokenGranters = new ArrayList(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;
   }
}

五種型別分別是:

  • ResourceOwnerPasswordTokenGranter ==> password密碼樣式

  • AuthorizationCodeTokenGranter ==> authorization_code授權碼樣式

  • ClientCredentialsTokenGranter ==> client_credentials客戶端樣式

  • ImplicitTokenGranter ==> implicit簡化樣式

  • RefreshTokenGranter ==>refresh_token 掃清token專用

以客戶端樣式為例,思考如何產生token的,則需要繼續研究5種授權者的抽象類:AbstractTokenGranter

public abstract class AbstractTokenGranter implements TokenGranter {
   protected final Log logger = LogFactory.getLog(getClass());
   //與token相關的service,重點
   private final AuthorizationServerTokenServices tokenServices;
   //與clientDetails相關的service,重點
   private final ClientDetailsService clientDetailsService;
   //建立oauth2Request的工廠,重點
   private final OAuth2RequestFactory requestFactory;

   private final String grantType;
   ...

   public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

       ...
       String clientId = tokenRequest.getClientId();
       ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
       validateGrantType(grantType, client);

       logger.debug("Getting access token for: " + clientId);

       return getAccessToken(client, tokenRequest);

   }

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

   protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
       OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
       return new OAuth2Authentication(storedOAuth2Request, null);
   }

   ...
}

回過頭去看TokenEndpoint中,正是呼叫了這裡的三個重要的類變數的相關方法。由於篇幅限制,不能延展太多,不然沒完沒了,所以重點分析下AuthorizationServerTokenServices是何方神聖。

AuthorizationServerTokenServices(瞭解)

AuthorizationServer端的token操作service,介面設計如下:

public interface AuthorizationServerTokenServices {
   //建立token
   OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
   //掃清token
   OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
           throws AuthenticationException
;
   //獲取token
   OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

}

在預設的實現類DefaultTokenServices中,可以看到token是如何產生的,並且瞭解了框架對token進行哪些資訊的關聯。

@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

   OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
   OAuth2RefreshToken refreshToken = null;
   if (existingAccessToken != null) {
       if (existingAccessToken.isExpired()) {
           if (existingAccessToken.getRefreshToken() != null) {
               refreshToken = existingAccessToken.getRefreshToken();
               // The token store could remove the refresh token when the
               // access token is removed, but we want to
               // be sure...
               tokenStore.removeRefreshToken(refreshToken);
           }
           tokenStore.removeAccessToken(existingAccessToken);
       }
       else {
           // Re-store the access token in case the authentication has changed
           tokenStore.storeAccessToken(existingAccessToken, authentication);
           return existingAccessToken;
       }
   }

   // Only create a new refresh token if there wasn't an existing one
   // associated with an expired access token.
   // Clients might be holding existing refresh tokens, so we re-use it in
   // the case that the old access token
   // expired.
   if (refreshToken == null) {
       refreshToken = createRefreshToken(authentication);
   }
   // But the refresh token itself might need to be re-issued if it has
   // expired.
   else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
       ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
       if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
           refreshToken = createRefreshToken(authentication);
       }
   }

   OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
   tokenStore.storeAccessToken(accessToken, authentication);
   // In case it was modified
   refreshToken = accessToken.getRefreshToken();
   if (refreshToken != null) {
       tokenStore.storeRefreshToken(refreshToken, authentication);
   }
   return accessToken;

}

簡單總結一下AuthorizationServerTokenServices的作用,他提供了建立token,掃清token,獲取token的實現。在建立token時,他會呼叫tokenStore對產生的token和相關資訊儲存到對應的實現類中,可以是redis,資料庫,記憶體,jwt。

總結

本篇總結了使用客戶端樣式獲取Token時,spring security oauth2內部的運作流程,重點是在分析AuthenticationServer相關的類。其他樣式有一定的不同,但抽象功能是固定的,只是具體的實現類會被相應地替換。閱讀spring的原始碼,會發現它的設計中出現了非常多的抽象介面,這對我們理清楚內部工作流程產生了不小的困擾,我的方式是可以藉助UML類圖,先從宏觀理清楚作者的設計思路,這會讓我們的分析事半功倍。

下一篇文章重點分析使用者攜帶token訪問受限資源時,spring security oauth2內部的工作流程。即ResourceServer相關的類。

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖