引言: 本文系《認證鑒權與API權限控制在微服務架構中的設計與實現》系列的第一篇,本系列預計四篇文章講解微服務下的認證鑒權與API權限控制的實現。
1. 背景
最近在做權限相關服務的開發,在系統微服務化后,原有的單體應用是基於Session的安全權限方式,不能滿足現有的微服務架構的認證與鑒權需求。微服務架構下,一個應用會被拆分成若干個微應用,每個微應用都需要對訪問進行鑒權,每個微應用都需要明確當前訪問用戶以及其權限。尤其當訪問來源不只是瀏覽器,還包括其他服務的調用時,單體應用架構下的鑒權方式就不是特別合適了。在微服務架構下,要考慮外部應用接入的場景、用戶–服務的鑒權、服務–服務的鑒權等多種鑒權場景。
比如用戶A訪問User Service,A如果未登錄,則首先需要登錄,請求獲取授權token。獲取token之后,A將攜帶着token去請求訪問某個文件,這樣就需要對A的身份進行校驗,並且A可以訪問該文件。
為了適應架構的變化、需求的變化,auth權限模塊被單獨出來作為一個基礎的微服務系統,為其他業務service提供服務。
2. 系統架構的變更
單體應用架構到分布式架構,簡化的權限部分變化如下面兩圖所示。
(1)單體應用簡化版架構圖:
(2)分布式應用簡化版架構圖:
分布式架構,特別是微服務架構的優點是可以清晰的划分出業務邏輯來,讓每個微服務承擔職責單一的功能,畢竟越簡單的東西越穩定。
但是,微服務也帶來了很多的問題。比如完成一個業務操作,需要跨很多個微服務的調用,那么如何用權限系統去控制用戶對不同微服務的調用,對我們來說是個挑戰。當業務微服務的調用接入權限系統后,不能拖累它們的吞吐量,當權限系統出現問題后,不能阻塞它們的業務調用進度,當然更不能改變業務邏輯。新的業務微服務快速接入權限系統相對容易把控,那么對於公司已有的微服務,如何能不改動它們的架構方式的前提下,快速接入,對我們來說,也是一大挑戰。
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系統的最終使用組件如下:
4.2 步驟
主要步驟為:
- 配置資源服務器和認證服務器
- 配置Spring Security
上述步驟比較籠統,對於前面小節提到的需求,屬於Auth系統的主要內容,筆者后面會另寫文章對應講解。
4.3 endpoint
提供的endpoint:
4.4 maven依賴
主要的jar包,pom.xml文件如下:
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
。
1 @Override 2 public void configure(HttpSecurity http) throws Exception { 3 http.csrf().disable() 4 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 5 .and() 6 .requestMatchers().antMatchers("/**") 7 .and().authorizeRequests() 8 .antMatchers("/**").permitAll() 9 .anyRequest().authenticated() 10 .and().logout() 11 .logoutUrl("/logout") 12 .clearAuthentication(true) 13 .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()) 14 .addLogoutHandler(customLogoutHandler());
4.7 執行endpoint
1. 首先執行獲取授權的endpoint。
上述構造了一個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
1 method: post 2 url: http://localhost:12000/oauth/check_token 3 header: 4 { 5 Authorization: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=, 6 Content-Type: application/x-www-form-urlencoded 7 } 8 body: 9 { 10 token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ.5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo 11 }
上面即為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進行清空。
5. 總結
本文是《認證鑒權與API權限控制在微服務架構中的設計與實現》系列文章的總述,從遇到的問題着手,介紹了項目的背景。通過調研現有的技術,並結合當前項目的實際,確定了技術選型。最后對於系統的最終的實現進行展示。后面將從實現的細節,講解本系統的實現。敬請期待后續文章。
參考