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

微服務網關netflix-zuul

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

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

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

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


引言: 本文系《認證鑑權與API權限控制在微服務架構中的設計與實現》系列的第一篇,本系列預計四篇文章講解微服務下的認證鑑權與API權限控制的實現。

1. 背景

最近在做權限相關服務的開發,在系統微服務化後,原有的單體應用是基於session的安全權限方式,不能滿足現有的微服務架構的認證與鑑權需求。微服務架構下,一個應用會被拆分成若干個微應用,每個微應用都需要對訪問進行鑑權,每個微應用都需要明確當前訪問用戶以及其權限。尤其當訪問來源不只是瀏覽器,還包括其他服務的調用時,單體應用架構下的鑑權方式就不是特別合適了。在微服務架構下,要考慮外部應用接入的場景、用戶–服務的鑑權、服務–服務的鑑權等多種鑑權場景。
比如用戶A訪問User Service,A如果未登錄,則首先需要登錄,請求獲取授權token。獲取token之後,A將攜帶着token去請求訪問某個文件,這樣就需要對A的身份進行校驗,並且A可以訪問該文件。
爲了適應架構的變化、需求的變化,auth權限模塊被單獨出來作爲一個基礎的微服務系統,爲其他業務service提供服務。

2. 系統架構的變更

單體應用架構到分佈式架構,簡化的權限部分變化如下面兩圖所示。
(1)單體應用簡化版架構圖:
single

單體架構


(2)分佈式應用簡化版架構圖:
distrubted

分佈式架構

分佈式架構,特別是微服務架構的優點是可以清晰的劃分出業務邏輯來,讓每個微服務承擔職責單一的功能,畢竟越簡單的東西越穩定。

但是,微服務也帶來了很多的問題。比如完成一個業務操作,需要跨很多個微服務的調用,那麼如何用權限系統去控制用戶對不同微服務的調用,對我們來說是個挑戰。當業務微服務的調用接入權限系統後,不能拖累它們的吞吐量,當權限系統出現問題後,不能阻塞它們的業務調用進度,當然更不能改變業務邏輯。新的業務微服務快速接入權限系統相對容易把控,那麼對於公司已有的微服務,如何能不改動它們的架構方式的前提下,快速接入,對我們來說,也是一大挑戰。

3. 技術方案

這主要包括兩方面需求:其一是認證與鑑權,對於請求的用戶身份的授權以及合法性鑑權;其二是API級別的操作權限控制,這個在第一點之後,當鑑定完用戶身份合法之後,對於該用戶的某個具體請求是否具有該操作執行權限進行校驗。

3.1 認證與鑑權

對於第一個需求,筆者調查了一些實現方案:

  1. 分佈式Session方案
    分佈式會話方案原理主要是將關於用戶認證的信息存儲在共享存儲中,且通常由用戶會話作爲 key 來實現的簡單分佈式哈希映射。當用戶訪問微服務時,用戶數據可以從共享存儲中獲取。在某些場景下,這種方案很不錯,用戶登錄狀態是不透明的。同時也是一個高可用且可擴展的解決方案。這種方案的缺點在於共享存儲需要一定保護機制,因此需要通過安全鏈接來訪問,這時解決方案的實現就通常具有相當高的複雜性了。

  2. 基於OAuth2 Token方案
    隨着 Restful API、微服務的興起,基於Token的認證現在已經越來越普遍。Token和Session ID 不同,並非只是一個 key。Token 一般會包含用戶的相關信息,通過驗證 Token 就可以完成身份校驗。用戶輸入登錄信息,發送到身份認證服務進行認證。AuthorizationServer驗證登錄信息是否正確,返回用戶基礎信息、權限範圍、有效時間等信息,客戶端存儲接口。用戶將 Token 放在 HTTP 請求頭中,發起相關 API 調用。被調用的微服務,驗證Token。ResourceServer返回相關資源和數據。

這邊選用了第二種方案,基於OAuth2 Token認證的好處如下:

  • 服務端無狀態:Token 機制在服務端不需要存儲 session 信息,因爲 Token 自身包含了所有用戶的相關信息。
  • 性能較好,因爲在驗證 Token 時不用再去訪問數據庫或者遠程服務進行權限校驗,自然可以提升不少性能。
  • 現在很多應用都是同時面向移動端和web端,OAuth2 Token機制可以支持移動設備。
  • OAuth2與Spring Security結合使用,有提供很多開箱即用的功能,大多特性都可以通過配置靈活的變更。
  • 最後一點,也很重要,Spring Security OAuth2的文檔寫得較爲詳細。

oauth2根據使用場景不同,分成了4種模式:

  • 授權碼模式(authorization code)
  • 簡化模式(implicit)
  • 密碼模式(resource owner password credentials)
  • 客戶端模式(client credentials)

對於上述oauth2四種模式不熟的同學,可以自行百度oauth2,阮一峯的文章有解釋。常使用的是password模式和client模式。

3.2 操作權限控制

對於第二個需求,筆者主要看了Spring Security和Shiro。

  1. Shiro
    Shiro是一個強大而靈活的開源安全框架,能夠非常清晰的處理認證、授權、管理會話以及密碼加密。Shiro很容易入手,上手快控制粒度可糙可細。自由度高,Shiro既能配合Spring使用也可以單獨使用。

  2. Spring Security
    Spring社區生態很強大。除了不能脫離Spring,Spring Security具有Shiro所有的功能。而且Spring Security對Oauth、OpenID也有支持,Shiro則需要自己手動實現。Spring Security的權限細粒度更高。但是Spring Security太過複雜。

看了下網上的評論,貌似一邊倒向Shiro。大部分人提出的Spring Security問題就是比較複雜難懂,文檔太長。不管是Shiro還是Spring Security,其實現都是基於過濾器,對於自定義實現過濾器,我想對於很多開發者並不是很難,但是這需要團隊花費時間與封裝可用的jar包出來,對於後期維護和升級,以及功能的擴展。很多中小型公司並不一定具有這樣的時間和人力投入這件事。筆者綜合評估了下複雜性與所要實現的權限需求,以及上一個需求調研的結果,既然Spring Security功能足夠強大且穩定,最終選擇了Spring Security

4. 系統架構

4.1 組件

Auth系統的最終使用組件如下:

OAuth2.0 JWT Token
Spring Security
Spring boot

4.2 步驟

主要步驟爲:

  • 配置資源服務器和認證服務器
  • 配置Spring Security

上述步驟比較籠統,對於前面小節提到的需求,屬於Auth系統的主要內容,筆者後面會另寫文章對應講解。

4.3 endpoint

提供的endpoint:

/oauth/token?grant_type=password #請求授權token

/oauth/token?grant_type=refresh_token #刷新token

/oauth/check_token #校驗token

/logout #註銷token及權限相關信息

4.4 maven依賴

主要的jar包,pom.xml文件如下:

<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>2.2.0</version>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-security</artifactId>
	<version>1.2.1-SNAPSHOT</version>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-oauth2</artifactId>
	<version>1.2.1-SNAPSHOT</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jersey</artifactId>
	<version>1.5.3.RELEASE</version>
</dependency>

 

4.5 AuthorizationServer配置文件

AuthorizationServer配置主要是覆寫如下的三個方法,分別針對endpoints、clients、security配置。

@Override

public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {

security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");

}

@Override

public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

//配置客戶端認證

clients.withClientDetails(clientDetailsService(dataSource));

}

@Override

public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

//配置token的數據源、自定義的tokenServices等信息

endpoints.authenticationManager(authenticationManager)

.tokenStore(tokenStore(dataSource))

.tokenServices(authorizationServerTokenServices())

.accessTokenConverter(accessTokenConverter())

.exceptionTranslator(webResponseExceptionTranslator);

}

4.6 ResourceServer配置

資源服務器的配置,覆寫了默認的配置。爲了支持logout,這邊自定義了一個CustomLogoutHandler並且將logoutSuccessHandler指定爲返回http狀態的HttpStatusReturningLogoutSuccessHandler

@Override

public void configure(HttpSecurity http) throws Exception {

http.csrf().disable()

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

.and()

.requestMatchers().antMatchers("/**")

.and().authorizeRequests()

.antMatchers("/**").permitAll()

.anyRequest().authenticated()

.and().logout()

.logoutUrl("/logout")

.clearAuthentication(true)

.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())

.addLogoutHandler(customLogoutHandler());

4.7 執行endpoint

  1. 首先執行獲取授權的endpoint。
    method: post
    
    url: http://localhost:12000/oauth/token?grant_type=password
    
    header:
    
    {
    
    Authorization: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=,
    
    Content-Type: application/x-www-form-urlencoded
    
    }
    
    body:
    
    {
    
    username: keets,
    
    password: ***
    
    }

     

  2. 上述構造了一個post請求,具體請求寫得很詳細。username和password是客戶端提供給服務器進行校驗用戶身份信息。header裏面的Authorization是存放的clientId和clientSecret經過編碼的字符串。

返回結果如下:

{

"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"

}

可以看到在用戶名密碼通過校驗後,客戶端收到了授權服務器的response,主要包括access token、refresh token。並且表明token的類型爲bearer,過期時間expires_in。筆者在jwt token中加入了自定義的info爲UserId和ClientId。

2.鑑權的endpoint

method: post

url: http://localhost:12000/oauth/check_token

header:

{

Authorization: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=,

Content-Type: application/x-www-form-urlencoded

}

body:

{

token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ.5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo

}

上面即爲check_token請求的詳細信息。需要注意的是,筆者將剛剛授權的token放在了body裏面,這邊可以有多種方法,此處不擴展。

{

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

"user_name": "keets",

"scope": [

"all"

],

"active": true,

"exp": 1508447756,

"X-KEETS-ClientId": "frontend",

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

"client_id": "frontend"

}

校驗token合法後,返回的response如上所示。在response中也是展示了相應的token中的基本信息。

3.刷新token
由於token的時效一般不會很長,而refresh token一般週期會很長,爲了不影響用戶的體驗,可以使用refresh token去動態的刷新token。

method: post

url: http://localhost:12000/oauth/token?grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsInVzZXJfbmFtZSI6ImtlZXRzIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImJhZDcyYjE5LWQ5ZjMtNDkwMi1hZmZhLTA0MzBlN2RiNzllZCIsImV4cCI6MTUxMDk5NjU1NiwianRpIjoiYWE0MWY1MjctODE3YS00N2UyLWFhOTgtZjNlMDZmNmY0NTZlIiwiY2xpZW50X2lkIjoiZnJvbnRlbmQifQ.mICT1-lxOAqOU9M-Ud7wZBb4tTux6OQWouQJ2nn1DeE

header:

{

Authorization: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=

}

其response和/oauth/token得到正常的相應是一樣的,此處不再列出。

4.註銷token

method: get

url: http://localhost:9000/logout

header:

{

Authorization: bearereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgzMzkwNTQsIlgtU0lNVS1Vc2VySWQiOiIwOGFhMTYxYi1lYjI3LTQ2NjAtYjA1MC1lMDc5YTJiODBhODMiLCJ1c2VyX25hbWUiOiJrZWV0cyIsImp0aSI6IjJhNTQ4NjY2LTRjNzEtNGEzNi1hZmY0LTMwZTI1Mjc0ZjQxZSIsImNsaWVudF9pZCI6ImZyb250ZW5kIiwic2NvcGUiOlsibWVua29yIl19.rA-U2iXnjH0AdPaGuvSEJH3bTth6AT3oQrGsKIams30

}

註銷成功則會返回200,註銷端點主要是將token和SecurityContextHolder進行清空。

5. 總結

本文是《認證鑑權與API權限控制在微服務架構中的設計與實現》系列文章的總述,從遇到的問題着手,介紹了項目的背景。通過調研現有的技術,並結合當前項目的實際,確定了技術選型。最後對於系統的最終的實現進行展示。後面將從實現的細節,講解本系統的實現。敬請期待後續文章。

 

 

作者:aoho
鏈接: http://blueskykong.com/2017/10/19/security1/

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