認證鑑權與API權限控制在微服務架構中的設計與實現(二)

微服務網關netflix-zuul

微服務架構中整合網關、權限服務

認證鑑權與API權限控制在微服務架構中的設計與實現(一)

認證鑑權與API權限控制在微服務架構中的設計與實現(三)

認證鑑權與API權限控制在微服務架構中的設計與實現(四)


 

引言: 本文系《認證鑑權與API權限控制在微服務架構中的設計與實現》系列的第二篇,本文重點講解用戶身份的認證與token發放的具體實現。本文篇幅較長,對涉及到的大部分代碼進行了分析,可收藏於閒暇時間閱讀,歡迎訂閱本系列文章。

1. 系統概覽

在上一篇 認證鑑權與API權限控制在微服務架構中的設計與實現(一)介紹了該項目的背景以及技術調研與最後選型,並且對於最終實現的endpoint執行結果進行展示。對系統架構雖然有提到,但是並未列出詳細流程圖。在筆者的應用場景中,Auth系統與網關進行結合。在網關出配置相應的端點信息,如登錄系統申請token授權,校驗check_token等端點。

下圖爲網關與Auth系統結合的流程圖,網關係統的具體實現細節在後面另寫文章介紹。(此處流程圖的繪製中,筆者使用極簡的語言描述,各位同學輕噴😆!)

login

授權流程圖

上圖展示了系統登錄的簡單流程,其中的細節有省略,用戶信息的合法性校驗實際是調用用戶系統。大體流程是這樣,客戶端請求到達網關之後,根據網關識別的請求登錄端點,轉發到Auth系統,將用戶的信息進行校驗。

另一方面是對於一般請求的校驗。一些不需要權限的公開接口,在網關處配置好,請求到達網關後,匹配了路徑將會直接放行。如果需要對該請求進行校驗,會將該請求的相關驗證信息截取,以及API權限校驗所需的上下文信息(筆者項目對於一些操作進行權限前置驗證,下一篇章會講到),調用Auth系統,校驗成功後進行路由轉發。

gw

身份及API權限校驗的流程圖

這篇文章就重點講解我們在第一篇文章中提到的用戶身份的認證與token發放。這個也主要包含兩個方面:

  • 用戶合法性的認證
  • 獲取到授權的token

2. 配置與類圖

2.1 AuthorizationServer主要配置

關於AuthorizationServerResourceServer的配置在上一篇文章已經列出。AuthorizationServer主要是繼承了AuthorizationServerConfigurerAdapter,覆寫了其實現接口的三個方法:

//對應於配置AuthorizationServer安全認證的相關信息,創建ClientCredentialsTokenEndpointFilter核心過濾器

@Override

public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {

}

//配置OAuth2的客戶端相關信息

@Override

public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

}

//配置身份認證器,配置認證方式,TokenStore,TokenGranter,OAuth2RequestFactory

@Override

public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

}


2.2 主要Authentication類的類圖

AuthorizationServer UML類圖

主要的驗證方法authenticate(Authentication authentication)在接口AuthenticationManager中,其實現類有ProviderManager,有上圖可以看出ProviderManager又依賴於AuthenticationProvider接口,其定義了一個List<AuthenticationProvider>全局變量。筆者這邊實現了該接口的實現類CustomAuthenticationProvider。自定義一個provider,並在GlobalAuthenticationConfigurerAdapter中配置好改自定義的校驗provider,覆寫configure()方法。

@Configuration

public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {

@Autowired

CustomAuthenticationProvider customAuthenticationProvider;

@Override

public void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.authenticationProvider(customAuthenticationProvider);//使用自定義的AuthenticationProvider

}

}

 

AuthenticationManagerBuilder是用來創建AuthenticationManager,允許自定義提供多種方式的AuthenticationProvider,比如LDAP、基於JDBC等等。

3. 認證與授權token

下面講解認證與授權token主要的類與接口。

3.1 內置端點TokenEndpoint

Spring-Security-Oauth2的提供的jar包中內置了與token相關的基礎端點。本文認證與授權token與/oauth/token有關,其處理的接口類爲TokenEndpoint。下面我們來看一下對於認證與授權token流程的具體處理過程。

@FrameworkEndpoint

public class TokenEndpoint extends AbstractEndpoint {

...

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)

public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam

Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

//首先對client信息進行校驗

if (!(principal instanceof Authentication)) {

throw new InsufficientAuthenticationException(

"There is no client authentication. Try adding an appropriate authentication filter.");

}

String clientId = getClientId(principal);

//根據請求中的clientId,加載client的具體信息

ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

...

//驗證scope域範圍

if (authenticatedClient != null) {

oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);

}

//授權方式不能爲空

if (!StringUtils.hasText(tokenRequest.getGrantType())) {

throw new InvalidRequestException("Missing grant type");

}

//token endpoint不支持Implicit模式

if (tokenRequest.getGrantType().equals("implicit")) {

throw new InvalidGrantException("Implicit grant type not supported from token endpoint");

}

...

//進入CompositeTokenGranter,匹配授權模式,然後進行password模式的身份驗證和token的發放

OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

if (token == null) {

throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());

}

return getResponse(token);

}

...

}

上面給代碼進行了註釋,讀者感興趣可以看看。接口處理的主要流程就是對authentication信息進行檢查是否合法,不合法直接拋出異常,然後對請求的GrantType進行處理,根據GrantType,進行password模式的身份驗證和token的發放。下面我們來看下TokenGranter的類圖。
endpoint

granter

TokenGranter

可以看出TokenGranter的實現類CompositeTokenGranter中有一個List<TokenGranter>,對應五種GrantType的實際授權實現。這邊涉及到的getTokenGranter(),代碼也列下:

public class CompositeTokenGranter implements TokenGranter {

//GrantType的集合,有五種,之前有講

private final List<TokenGranter> tokenGranters;

public CompositeTokenGranter(List<TokenGranter> tokenGranters) {

this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);

}

//遍歷list,匹配到相應的grantType就進行處理

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

for (TokenGranter granter : tokenGranters) {

OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);

if (grant!=null) {

return grant;

}

}

return null;

}

...

}

 

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

if (!this.grantType.equals(grantType)) {

return null;

}

String clientId = tokenRequest.getClientId();

//加載clientId對應的ClientDetails,爲了下一步的驗證

ClientDetails client = clientDetailsService.loadClientByClientId(clientId);

//再次驗證clientId是否擁有該grantType模式,安全

validateGrantType(grantType, client);

//獲取token

return getAccessToken(client, tokenRequest);

}

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {

//進入創建token之前,進行身份驗證

return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));

}

protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

//身份驗證

OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);

return new OAuth2Authentication(storedOAuth2Request, null);

}

 

本次請求是使用的password模式,隨後進入其GrantType具體的處理流程,下面是grant()方法。

上面一段代碼是grant()方法具體的實現細節。GrantType匹配到其對應的grant()後,先進行基本的驗證確保安全,然後進入主流程,就是下面小節要講的驗證身份和發放token。

3.2 自定義的驗證類CustomAuthenticationProvider

CustomAuthenticationProvider中定義了驗證方法的具體實現。其具體實現如下所示。

//主要的自定義驗證方法

@Override

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

String username = authentication.getName();

String password = (String) authentication.getCredentials();

Map data = (Map) authentication.getDetails();

String clientId = (String) data.get("client");

Assert.hasText(clientId,"clientId must have value" );

String type = (String) data.get("type");

//通過調用user服務,校驗用戶信息

Map map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));

//校驗返回的信息,不正確則拋出異常,授權失敗

String userId = (String) map.get("userId");

if (StringUtils.isBlank(userId)) {

String errorCode = (String) map.get("code");

throw new BadCredentialsException(errorCode);

}

CustomUserDetails customUserDetails = buildCustomUserDetails(username, password, userId, clientId);

return new CustomAuthenticationToken(customUserDetails);

}

//構造一個CustomUserDetails,簡單,略去

private CustomUserDetails buildCustomUserDetails(String username, String password, String userId, String clientId) {

}

//構造一個請求userService的map,內容略

private Map<String, String> getUserServicePostObject(String username, String password, String type) {

}

 

authenticate()最後返回構造的自定義CustomAuthenticationToken,在CustomAuthenticationToken中,將boolean authenticated設爲true,user信息驗證成功。這邊傳入的參數CustomUserDetails與token生成有關,作爲payload中的信息,下面會講到。

//繼承抽象類AbstractAuthenticationToken

public class CustomAuthenticationToken extends AbstractAuthenticationToken {

private CustomUserDetails userDetails;

public CustomAuthenticationToken(CustomUserDetails userDetails) {

super(null);

this.userDetails = userDetails;

super.setAuthenticated(true);

}

...

}

AbstractAuthenticationToken實現了接口Authentication和CredentialsContainer,裏面的具體信息讀者可以自己看下源碼。

3.3 關於JWT

用戶信息校驗完成之後,下一步則是要對該用戶進行授權。在講具體的授權之前,先補充下關於JWT Token的相關知識點。

Json web token (JWT), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準(RFC 7519)。該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。

從上面的描述可知JWT的定義,這邊讀者可以對比下token的認證和傳統的session認證的區別。推薦一篇文章什麼是 JWT – JSON WEB TOKEN,筆者這邊就不詳細擴展講了,只是簡單介紹下其構成。

JWT包含三部分:header頭部、payload信息、signature簽名。下面以上一篇生成好的access_token爲例介紹。

  

  • header
    jwt的頭部承載兩部分信息,一是聲明類型,這裏是jwt;二是聲明加密的算法 通常直接使用 HMAC SHA256。第一部分一般固定爲:

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

     

  • playload
    存放的有效信息,這些有效信息包含三個部分、標準中註冊的聲明、公共的聲明、私有的聲明。這邊筆者額外添加的信息爲X-KEETS-UserIdX-KEETS-ClientId。讀者可根據實際項目需要進行定製。最後playload經過base64編碼後的結果爲:

      

eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ
  • signature

      jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:header (base64後的)、payload (base64後的)、secret。
      關於secret,細心的讀者可能會發現之前的配置裏面有具體設置。前兩部分連接組成的字符串,通過header中聲明的加密方         式進行加鹽secret組合加密,然後就構成了jwt的第三部分。第三部分結果爲:

     

5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo

至於具體應用方法,可以參見第一篇文章中構建的/logout端點。

3.3 自定義的AuthorizationTokenServices

現在到了爲用戶創建token,這邊主要與自定義的接口AuthorizationServerTokenServices有關。AuthorizationServerTokenServices主要有如下三個方法:

//創建token

OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;

//刷新token

OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)

throws AuthenticationException;

//獲取token

OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

由於篇幅限制,筆者這邊僅對createAccessToken()的實現方法進行分析,其他的方法實現,讀者可以下關注筆者的GitHub項目。

public class CustomAuthorizationTokenServices implements AuthorizationServerTokenServices, ConsumerTokenServices {

...

public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

//通過TokenStore,獲取現存的AccessToken

OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);

OAuth2RefreshToken refreshToken;

//移除已有的AccessToken和refreshToken

if (existingAccessToken != null) {

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);

}

//recreate a refreshToken

refreshToken = createRefreshToken(authentication);

OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);

if (accessToken != null) {

tokenStore.storeAccessToken(accessToken, authentication);

}

refreshToken = accessToken.getRefreshToken();

if (refreshToken != null) {

tokenStore.storeRefreshToken(refreshToken, authentication);

}

return accessToken;

}

...

}

 

這邊具體的實現在上面有註釋,基本沒有改寫多少,讀者此處可以參閱源碼。createAccessToken()還調用了兩個私有方法,分別創建accessToken和refreshToken。創建accessToken,需要基於refreshToken。
此處可以自定義設置token的時效長度,accessToken創建實現如下:

private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.

private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.

private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {

//對應tokenId,存儲的標識

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);

//scope對應作用範圍

token.setScope(authentication.getOAuth2Request().getScope());

//上一節介紹的自定義TokenEnhancer,這邊使用

return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;

}

 

既然提到TokenEnhancer,這邊簡單貼一下代碼。

private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.

private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.

private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {

//對應tokenId,存儲的標識

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);

//scope對應作用範圍

token.setScope(authentication.getOAuth2Request().getScope());

//上一節介紹的自定義TokenEnhancer,這邊使用

return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;

}

自此,用戶身份校驗與發放授權token結束。最終成功返回的結果爲:

{

"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ.5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo",

"token_type": "bearer",

"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsInVzZXJfbmFtZSI6ImtlZXRzIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImJhZDcyYjE5LWQ5ZjMtNDkwMi1hZmZhLTA0MzBlN2RiNzllZCIsImV4cCI6MTUxMDk5NjU1NiwianRpIjoiYWE0MWY1MjctODE3YS00N2UyLWFhOTgtZjNlMDZmNmY0NTZlIiwiY2xpZW50X2lkIjoiZnJvbnRlbmQifQ.mICT1-lxOAqOU9M-Ud7wZBb4tTux6OQWouQJ2nn1DeE",

"expires_in": 43195,

"scope": "all",

"X-KEETS-UserId": "d6448c24-3c4c-4b80-8372-c2d61868f8c6",

"jti": "bad72b19-d9f3-4902-affa-0430e7db79ed",

"X-KEETS-ClientId": "frontend"

}

4. 總結

本文開頭給出了Auth系統概述,畫出了簡要的登錄和校驗的流程圖,方便讀者能對系統的實現有個大概的瞭解。然後主要講解了用戶身份的認證與token發放的具體實現。對於其中主要的類和接口進行了分析與講解。下一篇文章主要講解token的鑑定和API級別的上下文權限校驗。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章