認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(一)

引言: 本文系《認(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)圖:

single
single

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

分布式架構(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)方案:

  1. 分布式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ù)雜性了。

  2. 基于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。

  1. Shiro
    Shiro是一個(gè)強(qiáng)大而靈活的開(kāi)源安全框架,能夠非常清晰的處理認(rèn)證、授權(quán)、管理會(huì)話以及密碼加密。Shiro很容易入手,上手快控制粒度可糙可細(xì)。自由度高,Shiro既能配合Spring使用也可以單獨(dú)使用。

  2. 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

  1. 首先執(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ù)文章。

歡迎關(guān)注我的公眾號(hào)

微信公眾號(hào)

參考

  1. 理解OAuth 2.0
  2. 微服務(wù)API級(jí)權(quán)限的技術(shù)架構(gòu)
  3. 微服務(wù)架構(gòu)下的安全認(rèn)證與鑒權(quán)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容