引言: 本文系《認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)》系列的第一篇,本系列預(yù)計(jì)四篇文章講解微服務(wù)下的認(rèn)證鑒權(quán)與API權(quán)限控制的實(shí)現(xiàn)。
1. 背景
最近在做權(quán)限相關(guān)服務(wù)的開(kāi)發(fā),在系統(tǒng)微服務(wù)化后,原有的單體應(yīng)用是基于session的安全權(quán)限方式,不能滿足現(xiàn)有的微服務(wù)架構(gòu)的認(rèn)證與鑒權(quán)需求。微服務(wù)架構(gòu)下,一個(gè)應(yīng)用會(huì)被拆分成若干個(gè)微應(yīng)用,每個(gè)微應(yīng)用都需要對(duì)訪問(wèn)進(jìn)行鑒權(quán),每個(gè)微應(yīng)用都需要明確當(dāng)前訪問(wèn)用戶以及其權(quán)限。尤其當(dāng)訪問(wèn)來(lái)源不只是瀏覽器,還包括其他服務(wù)的調(diào)用時(shí),單體應(yīng)用架構(gòu)下的鑒權(quán)方式就不是特別合適了。在微服務(wù)架構(gòu)下,要考慮外部應(yīng)用接入的場(chǎng)景、用戶--服務(wù)的鑒權(quán)、服務(wù)--服務(wù)的鑒權(quán)等多種鑒權(quán)場(chǎng)景。
比如用戶A訪問(wèn)User Service,A如果未登錄,則首先需要登錄,請(qǐng)求獲取授權(quán)token。獲取token之后,A將攜帶著token去請(qǐng)求訪問(wèn)某個(gè)文件,這樣就需要對(duì)A的身份進(jìn)行校驗(yàn),并且A可以訪問(wèn)該文件。
為了適應(yīng)架構(gòu)的變化、需求的變化,auth權(quán)限模塊被單獨(dú)出來(lái)作為一個(gè)基礎(chǔ)的微服務(wù)系統(tǒng),為其他業(yè)務(wù)service提供服務(wù)。
2. 系統(tǒng)架構(gòu)的變更
單體應(yīng)用架構(gòu)到分布式架構(gòu),簡(jiǎn)化的權(quán)限部分變化如下面兩圖所示。
(1)單體應(yīng)用簡(jiǎn)化版架構(gòu)圖:

(2)分布式應(yīng)用簡(jiǎn)化版架構(gòu)圖:

分布式架構(gòu),特別是微服務(wù)架構(gòu)的優(yōu)點(diǎn)是可以清晰的劃分出業(yè)務(wù)邏輯來(lái),讓每個(gè)微服務(wù)承擔(dān)職責(zé)單一的功能,畢竟越簡(jiǎn)單的東西越穩(wěn)定。
但是,微服務(wù)也帶來(lái)了很多的問(wèn)題。比如完成一個(gè)業(yè)務(wù)操作,需要跨很多個(gè)微服務(wù)的調(diào)用,那么如何用權(quán)限系統(tǒng)去控制用戶對(duì)不同微服務(wù)的調(diào)用,對(duì)我們來(lái)說(shuō)是個(gè)挑戰(zhàn)。當(dāng)業(yè)務(wù)微服務(wù)的調(diào)用接入權(quán)限系統(tǒng)后,不能拖累它們的吞吐量,當(dāng)權(quán)限系統(tǒng)出現(xiàn)問(wèn)題后,不能阻塞它們的業(yè)務(wù)調(diào)用進(jìn)度,當(dāng)然更不能改變業(yè)務(wù)邏輯。新的業(yè)務(wù)微服務(wù)快速接入權(quán)限系統(tǒng)相對(duì)容易把控,那么對(duì)于公司已有的微服務(wù),如何能不改動(dòng)它們的架構(gòu)方式的前提下,快速接入,對(duì)我們來(lái)說(shuō),也是一大挑戰(zhàn)。
3. 技術(shù)方案
這主要包括兩方面需求:其一是認(rèn)證與鑒權(quán),其二是API級(jí)別的操作權(quán)限控制。
3.1 認(rèn)證與鑒權(quán)
對(duì)于第一個(gè)需求,筆者調(diào)查了一些實(shí)現(xiàn)方案:
分布式
Session
方案
分布式會(huì)話方案原理主要是將關(guān)于用戶認(rèn)證的信息存儲(chǔ)在共享存儲(chǔ)中,且通常由用戶會(huì)話作為 key 來(lái)實(shí)現(xiàn)的簡(jiǎn)單分布式哈希映射。當(dāng)用戶訪問(wèn)微服務(wù)時(shí),用戶數(shù)據(jù)可以從共享存儲(chǔ)中獲取。在某些場(chǎng)景下,這種方案很不錯(cuò),用戶登錄狀態(tài)是不透明的。同時(shí)也是一個(gè)高可用且可擴(kuò)展的解決方案。這種方案的缺點(diǎn)在于共享存儲(chǔ)需要一定保護(hù)機(jī)制,因此需要通過(guò)安全鏈接來(lái)訪問(wèn),這時(shí)解決方案的實(shí)現(xiàn)就通常具有相當(dāng)高的復(fù)雜性了。基于
OAuth2 Token
方案
隨著 Restful API、微服務(wù)的興起,基于Token
的認(rèn)證現(xiàn)在已經(jīng)越來(lái)越普遍。Token和Session ID 不同,并非只是一個(gè) key。Token 一般會(huì)包含用戶的相關(guān)信息,通過(guò)驗(yàn)證 Token 就可以完成身份校驗(yàn)。用戶輸入登錄信息,發(fā)送到身份認(rèn)證服務(wù)進(jìn)行認(rèn)證。AuthorizationServer驗(yàn)證登錄信息是否正確,返回用戶基礎(chǔ)信息、權(quán)限范圍、有效時(shí)間等信息,客戶端存儲(chǔ)接口。用戶將 Token 放在 HTTP 請(qǐng)求頭中,發(fā)起相關(guān) API 調(diào)用。被調(diào)用的微服務(wù),驗(yàn)證Token
。ResourceServer返回相關(guān)資源和數(shù)據(jù)。
這邊選用了第二種方案,基于OAuth2 Token
認(rèn)證的好處如下:
- 服務(wù)端無(wú)狀態(tài):Token 機(jī)制在服務(wù)端不需要存儲(chǔ) session 信息,因?yàn)?Token 自身包含了所有用戶的相關(guān)信息。
- 性能較好,因?yàn)樵隍?yàn)證 Token 時(shí)不用再去訪問(wèn)數(shù)據(jù)庫(kù)或者遠(yuǎn)程服務(wù)進(jìn)行權(quán)限校驗(yàn),自然可以提升不少性能。
- 現(xiàn)在很多應(yīng)用都是同時(shí)面向移動(dòng)端和web端,
OAuth2 Token
機(jī)制可以支持移動(dòng)設(shè)備。 - 最后一點(diǎn),也是挺重要的,OAuth2與Spring Security結(jié)合使用,Spring Security OAuth2的文檔寫(xiě)得較為詳細(xì)。
oauth2根據(jù)使用場(chǎng)景不同,分成了4種模式:
- 授權(quán)碼模式(authorization code)
- 簡(jiǎn)化模式(implicit)
- 密碼模式(resource owner password credentials)
- 客戶端模式(client credentials)
對(duì)于上述oauth2四種模式不熟的同學(xué),可以自行百度oauth2,阮一峰的文章有解釋。常使用的是password模式和client模式。
3.2 操作權(quán)限控制
對(duì)于第二個(gè)需求,筆者主要看了Spring Security和Shiro。
Shiro
Shiro是一個(gè)強(qiáng)大而靈活的開(kāi)源安全框架,能夠非常清晰的處理認(rèn)證、授權(quán)、管理會(huì)話以及密碼加密。Shiro很容易入手,上手快控制粒度可糙可細(xì)。自由度高,Shiro既能配合Spring使用也可以單獨(dú)使用。Spring Security
Spring社區(qū)生態(tài)很強(qiáng)大。除了不能脫離Spring,Spring Security具有Shiro所有的功能。而且Spring Security對(duì)Oauth、OpenID也有支持,Shiro則需要自己手動(dòng)實(shí)現(xiàn)。Spring Security的權(quán)限細(xì)粒度更高。但是Spring Security太過(guò)復(fù)雜。
看了下網(wǎng)上的評(píng)論,貌似一邊倒向Shiro。大部分人提出的Spring Security
問(wèn)題就是比較復(fù)雜難懂,文檔太長(zhǎng)。筆者綜合評(píng)估了下復(fù)雜性與所要實(shí)現(xiàn)的權(quán)限需求,以及上一個(gè)需求調(diào)研的結(jié)果,最終選擇了Spring Security
。
4. 系統(tǒng)架構(gòu)
4.1 組件
Auth系統(tǒng)的最終使用組件如下:
OAuth2.0 JWT Token
Spring Security
Spring boot
4.2 步驟
主要步驟為:
- 配置資源服務(wù)器和認(rèn)證服務(wù)器
- 配置Spring Security
上述步驟比較籠統(tǒng),對(duì)于前面小節(jié)提到的需求,屬于Auth系統(tǒng)的主要內(nèi)容,筆者后面會(huì)另寫(xiě)文章對(duì)應(yīng)講解。
4.3 endpoint
提供的endpoint:
/oauth/token?grant_type=password #請(qǐng)求授權(quán)token
/oauth/token?grant_type=refresh_token #刷新token
/oauth/check_token #校驗(yàn)token
/logout #注銷token及權(quán)限相關(guān)信息
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配置主要是覆寫(xiě)如下的三個(gè)方法,分別針對(duì)endpoints、clients、security配置。
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//配置客戶端認(rèn)證
clients.withClientDetails(clientDetailsService(dataSource));
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//配置token的數(shù)據(jù)源、自定義的tokenServices等信息
endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore(dataSource))
.tokenServices(authorizationServerTokenServices())
.accessTokenConverter(accessTokenConverter())
.exceptionTranslator(webResponseExceptionTranslator);
}
4.6 ResourceServer配置
資源服務(wù)器的配置,覆寫(xiě)了默認(rèn)的配置。為了支持logout,這邊自定義了一個(gè)CustomLogoutHandler
并且將logoutSuccessHandler
指定為返回http狀態(tài)的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 執(zhí)行endpoint
- 首先執(zhí)行獲取授權(quán)的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: ***
}
上述構(gòu)造了一個(gè)post請(qǐng)求,具體請(qǐng)求寫(xiě)得很詳細(xì)。username和password是客戶端提供給服務(wù)器進(jìn)行校驗(yàn)用戶身份信息。header里面的Authorization是存放的clientId和clientSecret經(jīng)過(guò)編碼的字符串。
返回結(jié)果如下:
{
"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"
}
可以看到在用戶名密碼通過(guò)校驗(yàn)后,客戶端收到了授權(quán)服務(wù)器的response,主要包括access_ token、refresh_ token。并且表明token的類型為bearer,過(guò)期時(shí)間expires_in。筆者在jwt token中加入了自定義的info為UserId和ClientId。
2.鑒權(quán)的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請(qǐng)求的詳細(xì)信息。需要注意的是,筆者將剛剛授權(quán)的token放在了body里面,這邊可以有多種方法,此處不擴(kuò)展。
{
"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"
}
校驗(yàn)token合法后,返回的response如上所示。在response中也是展示了相應(yīng)的token中的基本信息。
3.刷新token
由于token的時(shí)效一般不會(huì)很長(zhǎng),而refresh_ token一般周期會(huì)很長(zhǎng),為了不影響用戶的體驗(yàn),可以使用refresh_ token去動(dòng)態(tài)的刷新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得到正常的相應(yīng)是一樣的,此處不再列出。
4.注銷token
method: get
url: http://localhost:9000/logout
header:
{
Authorization: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=
}
注銷成功則會(huì)返回200,注銷端點(diǎn)主要是將token和SecurityContextHolder進(jìn)行清空。
5. 總結(jié)
本文是《認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)》系列文章的總述,從遇到的問(wèn)題著手,介紹了項(xiàng)目的背景。通過(guò)調(diào)研現(xiàn)有的技術(shù),并結(jié)合當(dāng)前項(xiàng)目的實(shí)際,確定了技術(shù)選型。最后對(duì)于系統(tǒng)的最終的實(shí)現(xiàn)進(jìn)行展示。后面將從實(shí)現(xiàn)的細(xì)節(jié),講解本系統(tǒng)的實(shí)現(xiàn)。敬請(qǐng)期待后續(xù)文章。