本文轉載:http://blueskykong.com/2017/10/19/security1/
引言: 本文系《認證鑒權與API權限控制在微服務架構中的設計與實作》系列的第一篇,本系列預計四篇文章講解微服務下的認證鑒權與API權限控制的實作。
1. 背景
最近在做權限相關服務的開發,在系統微服務化後,原有的單體應用是基于session的安全權限方式,不能滿足現有的微服務架構的認證與鑒權需求。微服務架構下,一個應用會被拆分成若幹個微應用,每個微應用都需要對通路進行鑒權,每個微應用都需要明确目前通路使用者以及其權限。尤其當通路來源不隻是浏覽器,還包括其他服務的調用時,單體應用架構下的鑒權方式就不是特别合适了。在微服務架構下,要考慮外部應用接入的場景、使用者–服務的鑒權、服務–服務的鑒權等多種鑒權場景。
比如使用者A通路User Service,A如果未登入,則首先需要登入,請求擷取授權token。擷取token之後,A将攜帶着token去請求通路某個檔案,這樣就需要對A的身份進行校驗,并且A可以通路該檔案。
為了适應架構的變化、需求的變化,auth權限子產品被單獨出來作為一個基礎的微服務系統,為其他業務service提供服務。
2. 系統架構的變更
單體應用架構到分布式架構,簡化的權限部分變化如下面兩圖所示。
(1)單體應用簡化版架構圖:
認證鑒權與API權限控制在微服務架構中的設計與實作 (2)分布式應用簡化版架構圖:
認證鑒權與API權限控制在微服務架構中的設計與實作 分布式架構,特别是微服務架構的優點是可以清晰的劃分出業務邏輯來,讓每個微服務承擔職責單一的功能,畢竟越簡單的東西越穩定。
但是,微服務也帶來了很多的問題。比如完成一個業務操作,需要跨很多個微服務的調用,那麼如何用權限系統去控制使用者對不同微服務的調用,對我們來說是個挑戰。當業務微服務的調用接入權限系統後,不能拖累它們的吞吐量,當權限系統出現問題後,不能阻塞它們的業務調用進度,當然更不能改變業務邏輯。新的業務微服務快速接入權限系統相對容易把控,那麼對于公司已有的微服務,如何能不改動它們的架構方式的前提下,快速接入,對我們來說,也是一大挑戰。
3. 技術方案
這主要包括兩方面需求:其一是認證與鑒權,對于請求的使用者身份的授權以及合法性鑒權;其二是API級别的操作權限控制,這個在第一點之後,當鑒定完使用者身份合法之後,對于該使用者的某個具體請求是否具有該操作執行權限進行校驗。
3.1 認證與鑒權
對于第一個需求,筆者調查了一些實作方案:
- 分布式
Session
方案
分布式會話方案原理主要是将關于使用者認證的資訊存儲在共享存儲中,且通常由使用者會話作為 key 來實作的簡單分布式哈希映射。當使用者通路微服務時,使用者資料可以從共享存儲中擷取。在某些場景下,這種方案很不錯,使用者登入狀态是不透明的。同時也是一個高可用且可擴充的解決方案。這種方案的缺點在于共享存儲需要一定保護機制,是以需要通過安全連結來通路,這時解決方案的實作就通常具有相當高的複雜性了。
- 基于
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。
-
Shiro
Shiro是一個強大而靈活的開源安全架構,能夠非常清晰的處理認證、授權、管理會話以及密碼加密。Shiro很容易入手,上手快控制粒度可糙可細。自由度高,Shiro既能配合Spring使用也可以單獨使用。 -
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系統的最終使用元件如下:
1
2
3
| OAuth2.0 JWT Token
Spring Security
Spring boot
|
4.2 步驟
主要步驟為:
- 配置資源伺服器和認證伺服器
- 配置Spring Security
上述步驟比較籠統,對于前面小節提到的需求,屬于Auth系統的主要内容,筆者後面會另寫文章對應講解。
4.3 endpoint
提供的endpoint:
1
2
3
4
5
6
7
| /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檔案如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| <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配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| @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
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @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
- 首先執行擷取授權的endpoint。
1
2
3
4
5
6
7
8
9
10
11
12
| 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: ***
}
|
上述構造了一個post請求,具體請求寫得很詳細。username和password是用戶端提供給伺服器進行校驗使用者身份資訊。header裡面的Authorization是存放的clientId和clientSecret經過編碼的字元串。
傳回結果如下:
1
2
3
4
5
6
7
8
9
10
| {
"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
1
2
3
4
5
6
7
8
9
10
11
| 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裡面,這邊可以有多種方法,此處不擴充。
1
2
3
4
5
6
7
8
9
10
11
12
| {
"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。
1
2
3
4
5
6
| 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
1
2
3
4
5
6
| method: get
url: http://localhost:9000/logout
header:
{
Authorization: bearereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgzMzkwNTQsIlgtU0lNVS1Vc2VySWQiOiIwOGFhMTYxYi1lYjI3LTQ2NjAtYjA1MC1lMDc5YTJiODBhODMiLCJ1c2VyX25hbWUiOiJrZWV0cyIsImp0aSI6IjJhNTQ4NjY2LTRjNzEtNGEzNi1hZmY0LTMwZTI1Mjc0ZjQxZSIsImNsaWVudF9pZCI6ImZyb250ZW5kIiwic2NvcGUiOlsibWVua29yIl19.rA-U2iXnjH0AdPaGuvSEJH3bTth6AT3oQrGsKIams30
}
|
登出成功則會傳回200,登出端點主要是将token和SecurityContextHolder進行清空。
5. 總結
本文是《認證鑒權與API權限控制在微服務架構中的設計與實作》系列文章的總述,從遇到的問題着手,介紹了項目的背景。通過調研現有的技術,并結合目前項目的實際,确定了技術選型。最後對于系統的最終的實作進行展示。後面将從實作的細節,講解本系統的實作。敬請期待後續文章。
推薦閱讀
系列文章:認證鑒權與API權限控制在微服務架構中的設計與實作
參考
- 了解OAuth 2.0
- 微服務API級權限的技術架構
- 微服務架構下的安全認證與鑒權