引言: 本文系《認證鑑權與API權限控制在微服務架構中的設計與實現》系列的第一篇,本系列預計四篇文章講解微服務下的認證鑑權與API權限控制的實現。html
最近在作權限相關服務的開發,在系統微服務化後,原有的單體應用是基於session的安全權限方式,不能知足現有的微服務架構的認證與鑑權需求。微服務架構下,一個應用會被拆分紅若干個微應用,每一個微應用都須要對訪問進行鑑權,每一個微應用都須要明確當前訪問用戶以及其權限。尤爲當訪問來源不僅是瀏覽器,還包括其餘服務的調用時,單體應用架構下的鑑權方式就不是特別合適了。在微服務架構下,要考慮外部應用接入的場景、用戶--服務的鑑權、服務--服務的鑑權等多種鑑權場景。
好比用戶A訪問User Service,A若是未登陸,則首先須要登陸,請求獲取受權token。獲取token以後,A將攜帶着token去請求訪問某個文件,這樣就須要對A的身份進行校驗,而且A能夠訪問該文件。
爲了適應架構的變化、需求的變化,auth權限模塊被單獨出來做爲一個基礎的微服務系統,爲其餘業務service提供服務。java
單體應用架構到分佈式架構,簡化的權限部分變化以下面兩圖所示。
(1)單體應用簡化版架構圖:
web
分佈式架構,特別是微服務架構的優勢是能夠清晰的劃分出業務邏輯來,讓每一個微服務承擔職責單一的功能,畢竟越簡單的東西越穩定。 spring
可是,微服務也帶來了不少的問題。好比完成一個業務操做,須要跨不少個微服務的調用,那麼如何用權限系統去控制用戶對不一樣微服務的調用,對咱們來講是個挑戰。當業務微服務的調用接入權限系統後,不能拖累它們的吞吐量,當權限系統出現問題後,不能阻塞它們的業務調用進度,固然更不能改變業務邏輯。新的業務微服務快速接入權限系統相對容易把控,那麼對於公司已有的微服務,如何能不改動它們的架構方式的前提下,快速接入,對咱們來講,也是一大挑戰。數據庫
這主要包括兩方面需求:其一是認證與鑑權,對於請求的用戶身份的受權以及合法性鑑權;其二是API級別的操做權限控制,這個在第一點以後,當鑑定完用戶身份合法以後,對於該用戶的某個具體請求是否具備該操做執行權限進行校驗。 瀏覽器
對於第一個需求,筆者調查了一些實現方案:tomcat
分佈式Session
方案
分佈式會話方案原理主要是將關於用戶認證的信息存儲在共享存儲中,且一般由用戶會話做爲 key 來實現的簡單分佈式哈希映射。當用戶訪問微服務時,用戶數據能夠從共享存儲中獲取。在某些場景下,這種方案很不錯,用戶登陸狀態是不透明的。同時也是一個高可用且可擴展的解決方案。這種方案的缺點在於共享存儲須要必定保護機制,所以須要經過安全連接來訪問,這時解決方案的實現就一般具備至關高的複雜性了。 安全
基於OAuth2 Token
方案
隨着 Restful API、微服務的興起,基於Token
的認證如今已經愈來愈廣泛。Token和Session ID 不一樣,並不是只是一個 key。Token 通常會包含用戶的相關信息,經過驗證 Token 就能夠完成身份校驗。用戶輸入登陸信息,發送到身份認證服務進行認證。AuthorizationServer驗證登陸信息是否正確,返回用戶基礎信息、權限範圍、有效時間等信息,客戶端存儲接口。用戶將 Token 放在 HTTP 請求頭中,發起相關 API 調用。被調用的微服務,驗證Token
。ResourceServer返回相關資源和數據。bash
這邊選用了第二種方案,基於OAuth2 Token
認證的好處以下:服務器
OAuth2 Token
機制能夠支持移動設備。oauth2根據使用場景不一樣,分紅了4種模式:
對於上述oauth2四種模式不熟的同窗,能夠自行百度oauth2,阮一峯的文章有解釋。常使用的是password模式和client模式。
對於第二個需求,筆者主要看了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
問題就是比較複雜難懂,文檔太長。筆者綜合評估了下複雜性與所要實現的權限需求,以及上一個需求調研的結果,最終選擇了Spring Security
。
Auth系統的最終使用組件以下:
OAuth2.0 JWT Token
Spring Security
Spring boot複製代碼
主要步驟爲:
上述步驟比較籠統,對於前面小節提到的需求,屬於Auth系統的主要內容,筆者後面會另寫文章對應講解。
提供的endpoint:
/oauth/token?grant_type=password #請求受權token
/oauth/token?grant_type=refresh_token #刷新token
/oauth/check_token #校驗token
/logout #註銷token及權限相關信息複製代碼
主要的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>複製代碼
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);
}複製代碼
資源服務器的配置,覆寫了默認的配置。爲了支持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());複製代碼
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通過編碼的字符串。
返回結果以下:
{
"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: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=
}複製代碼
註銷成功則會返回200,註銷端點主要是將token和SecurityContextHolder進行清空。
本文是《認證鑑權與API權限控制在微服務架構中的設計與實現》系列文章的總述,從遇到的問題着手,介紹了項目的背景。經過調研現有的技術,並結合當前項目的實際,肯定了技術選型。最後對於系統的最終的實現進行展現。後面將從實現的細節,講解本系統的實現。敬請期待後續文章。