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

aoho發表於2017-10-19

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

1. 背景

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

2. 系統架構的變更

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

single
single

(2)分散式應用簡化版架構圖:
distrubted
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問題就是比較複雜難懂,文件太長。筆者綜合評估了下複雜性與所要實現的許可權需求,以及上一個需求調研的結果,最終選擇了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: ***
}複製程式碼

上述構造了一個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進行清空。

5. 總結

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

歡迎關注我的公眾號

微信公眾號
微信公眾號


參考

  1. 理解OAuth 2.0
  2. 微服務API級許可權的技術架構
  3. 微服務架構下的安全認證與鑑權

相關文章