天天看點

建構微服務-使用OAuth 2.0保護API接口

我們将建立一個新的微服務,命名為product-api,作為一個外部API(OAuth 術語為資源伺服器-Resource Server),并通過之前介紹過的Edge Server暴露為微服務,作為Token Relay,也就是轉發Client端的OAuth通路令牌到資源伺服器(Resource Server)。另外添加OAuth Authorization Server和一個OAuth Client,也就是服務消費方。

微服務操作模型

基于Spring Cloud和Netflix OSS 建構微服務-Part 1

基于Spring Cloud和Netflix OSS建構微服務,Part 2

在本文中,我們将使用OAuth 2.0,建立一個的安全API,可供外部通路Part 1和Part 2完成的微服務。

關于OAuth 2.0的更多資訊,可以通路介紹文檔:Parecki - OAuth 2 Simplified 和 Jenkov - OAuth 2.0 Tutorial ,或者規範文檔 IETF RFC 6749。

繼續完善Part 2的系統全貌圖,添加新的OAuth元件(辨別為紅色框):

建構微服務-使用OAuth 2.0保護API接口

我們将示範Client端如何使用4種标準的授權流程,從授權伺服器(Authorization Server)擷取通路令牌(Access Token),接着使用通路令牌對資源伺服器發起安全通路,如API。

備注:

1/ 保護外部API并不是微服務的特殊需求,是以本文适用于任何使用OAuth 2.0保護外部API的架構;

2/ 我們使用的輕量級OAuth授權系統僅适用于開發和測試環境。在實際應用中,需要替換為一個API平台,或者委托給社交網絡Facebook或Twitter的登入、授權流程。

3/ 為了降低複雜度,我們特意采用了HTTP協定。在實際的應用中,OAuth通信需要使用TLS,如HTTPS保護通信資料。

4/ 在前面的文章中,我們為了強調微服務和單體應用的差異性,每一個微服務單獨運作在獨立的程序中。

和在Part 2中一樣,我們使用Java SE 8、Git和Gradle通路源代碼,并進行編譯:

git clone https://github.com/callistaenterprise/blog-microservices.git

cd blog-microservices

git checkout -b B3 M3.1

./build-all.sh

如果運作在Windows平台,則執行相應的bat檔案-build-all.bat。

在Part 2的基礎中,新增了2個元件源碼,分别為OAuth Authorization Server,項目名為auth-server;另一個為OAuth Resource Server,項目名為product-api-service。

建構微服務-使用OAuth 2.0保護API接口

編譯輸出10條log消息:

BUILD SUCCESSFUL

檢視2個新元件是如何實作的,以及Edge Server是如何更新并支援傳遞OAuth通路令牌的。我們也會修改API的URL,以便于使用。

2.1 Gradle 依賴

為了使用OAuth 2.0,我們将引入開源項目:spring-cloud-security和spring-security-oauth2,添加如下依賴。

auth-server項目:

    compile("org.springframework.boot:spring-boot-starter-security")

    compile("org.springframework.security.oauth:spring-security-oauth2:2.0.6.RELEASE")

完整代碼,可檢視auth-server/build.gradle檔案。

product-api-service項目:

    compile("org.springframework.cloud:spring-cloud-starter-security:1.0.0.RELEASE")

完整代碼,可以檢視product-api-service/build.gradle檔案。

2.2 AUTH-SERVER

授權伺服器(Authorization Server)的實作比較簡單直接。可直接使用@EnableAuthorizationServer标注。接着使用一個配置類注冊已準許的Client端應用,指定client-id、client-secret、以及允許的授予流程和範圍:

  @EnableAuthorizationServer

  protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Override

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

      clients.inMemory()

        .withClient("acme")

        .secret("acmesecret")

        .authorizedGrantTypes("authorization_code", "refresh_token", "implicit", "password", "client_credentials")

        .scopes("webshop");

    }

  }

顯然這一方法僅适用于開發和測試場景模拟Client端應用的注冊流程,實際應用中采用OAuth Authorization Server,如LinkedIn或GitHub。

完整的代碼,可以檢視AuthserverApplication.java。

模拟真實環境中Identity Provider的使用者注冊(OAuth術語稱為Resource Owner),通過在檔案application.properties中,為每一個使用者添加一行文本,如:

security.user.password=password

完整代碼,可以檢視application.properties檔案。

實作代碼也提供了2個簡單的web使用者界面,用于使用者認證和使用者準許,詳細可以檢視源代碼:

https://github.com/callistaenterprise/blog-microservices/tree/B3/microservices/support/auth-server/src/main/resources/templates

2.3 PRODUCT-API-SERVICE

為了讓API代碼實作OAuth Resource Server的功能,我們隻需要在main方法上添加@EnableOAuth2Resource标注:

@EnableOAuth2Resource

public class ProductApiServiceApplication {

完整代碼,可以檢視ProductApiServiceApplication.java。

API服務代碼的實作和Part 2中的組合服務代碼的實作很相似。為了驗證OAuth工作正常,我們添加了user-id和access token的日志輸出:

@RequestMapping("/{productId}")

    @HystrixCommand(fallbackMethod = "defaultProductComposite")

    public ResponseEntity<String> getProductComposite(

        @PathVariable int productId,

        @RequestHeader(value="Authorization") String authorizationHeader,

        Principal currentUser) {

        LOG.info("ProductApi: User={}, Auth={}, called with productId={}",

          currentUser.getName(), authorizationHeader, productId);

        ...       

1/ Spring MVC 将自動填充額外的參數,如current user和authorization header。

2/ 為了URL更簡潔,我們從@RequestMapping中移除了/product。當使用Edge Server時,它會自動添加一個/product字首,并将請求路由到正确的服務。

3/ 在實際的應用中,不建議在log中輸出通路令牌(access token)。

2.4 更新Edge Server

最後,我們需要讓Edge Server轉發OAuth通路令牌到API服務。非常幸運的是,這是預設的行為,我們不必做任何事情。

為了讓URL更簡潔,我們修改了Part 2中的路由配置:

zuul:

  ignoredServices: "*"

  prefix: /api

  routes:

    productapi: /product/**

這樣,可以使用URL:http://localhost:8765/api/product/123,而不必像前面使用的URL:http://localhost:8765/productapi/product/123。

我們也替換了到composite-service的路由為到api-service的路由。

完整的代碼,可以檢視application.yml檔案。

首先啟動RabbitMQ:

$ ~/Applications/rabbitmq_server-3.4.3/sbin/rabbitmq-server

如在Windows平台,需要确認RabbitMQ服務已經啟動。

接着啟動基礎設施微服務:

$ cd support/auth-server;       ./gradlew bootRun

$ cd support/discovery-server;  ./gradlew bootRun

$ cd support/edge-server;       ./gradlew bootRun

$ cd support/monitor-dashboard; ./gradlew bootRun

$ cd support/turbine;           ./gradlew bootRun

最後,啟動業務微服務:

$ cd core/product-service;                ./gradlew bootRun

$ cd core/recommendation-service;         ./gradlew bootRun

$ cd core/review-service;                 ./gradlew bootRun

$ cd composite/product-composite-service; ./gradlew bootRun

$ cd api/product-api-service;             ./gradlew bootRun

如在Windows平台,可以執行相應的bat檔案-start-all.bat。

一旦微服務啟動完成,并注冊到服務發現伺服器(Service Discovery Server),會輸出如下日志:

DiscoveryClient ... - registration status: 204

現在已經準備好嘗試擷取通路令牌,并使用它安全地調用API接口。

OAuth 2.0規範定義了4種授予方式,擷取通路令牌:

建構微服務-使用OAuth 2.0保護API接口

更詳細資訊,可檢視Jenkov - OAuth 2.0 Authorization。

備注:Authorization Code 和Implicit是最常用的2種方式。如前面2種方式不使用,其他2種适用于一個特殊場景。

接下來看看每一個授予流程是如何擷取通路令牌的。

4.1 授權代碼許可(Authorization Code Grant)

首先,我們通過浏覽器擷取一個代碼許可:

http://localhost:9999/uaa/oauth/authorize? response_type=code& client_id=acme& redirect_uri=http://example.com& scope=webshop& state=97536

先登入(user/password),接着重定向到類似如下URL:

http://example.com/?

  code=IyJh4Y&

  state=97536

備注:在請求中state參數設定為一個随機值,在響應中進行檢查,避免cross-site request forgery攻擊。

從重定向的URL中擷取code參數,并儲存在環境變量中:

CODE=IyJh4Y

現在作為一個安全的web伺服器,使用code grant擷取通路令牌:

curl acme:acmesecret@localhost:9999/uaa/oauth/token \

 -d grant_type=authorization_code \

 -d client_id=acme \

 -d redirect_uri=http://example.com \

 -d code=$CODE -s | jq .

{

  "access_token": "eba6a974-3c33-48fb-9c2e-5978217ae727",

  "token_type": "bearer",

  "refresh_token": "0eebc878-145d-4df5-a1bc-69a7ef5a0bc3",

  "expires_in": 43105,

  "scope": "webshop"

}

在環境變量中儲存通路令牌,為随後通路API時使用:

TOKEN=eba6a974-3c33-48fb-9c2e-5978217ae727

再次嘗試使用相同的代碼擷取通路令牌,應該會失敗。因為code實際上是一次性密碼的工作方式。

  "error": "invalid_grant",

  "error_description": "Invalid authorization code: IyJh4Y"

4.2 隐式許可(Implicit Grant)

通過Implicit Grant,可以跳過前面的Code Grant。可通過浏覽器直接請求通路令牌。在浏覽器中使用如下URL位址:

http://localhost:9999/uaa/oauth/authorize? response_type=token& client_id=acme& redirect_uri=http://example.com& scope=webshop& state=48532

登入(user/password)并驗證通過,浏覽器重定向到類似如下URL:

http://example.com/#

 access_token=00d182dc-9f41-41cd-b37e-59de8f882703&

 token_type=bearer&

 state=48532&

 expires_in=42704

備注:在請求中state參數應該設定為一個随機,以便在響應中檢查,避免cross-site request forgery攻擊。

在環境變量中儲存通路令牌,以便随後通路API時使用:

TOKEN=00d182dc-9f41-41cd-b37e-59de8f882703

4.3 資源所有者密碼憑證許可(Resource Owner Password Credentials Grant)

在這一場景下,使用者不必通路web浏覽器,使用者在Client端應用中輸入憑證,通過該憑證擷取通路令牌(從安全角度而言,如果你不信任Client端應用,這不是一個好的辦法):

curl -s acme:acmesecret@localhost:9999/uaa/oauth/token  \

 -d grant_type=password \

 -d scope=webshop \

 -d username=user \

 -d password=password | jq .

  "access_token": "62ca1eb0-b2a1-4f66-bcf4-2c0171bbb593",

  "refresh_token": "920fd8e6-1407-41cd-87ad-e7a07bd6337a",

  "expires_in": 43173,

在環境變量中儲存通路令牌,以便在随後通路API時使用:

TOKEN=62ca1eb0-b2a1-4f66-bcf4-2c0171bbb593

4.4 Client端憑證許可(Client Credentials Grant)

在最後一種情況下,我們假定使用者不必準許就可以通路API。在這種情況下,Client端應用進行驗證自己的授權伺服器,并擷取通路令牌:

 -d grant_type=client_credentials \

 -d scope=webshop | jq .

  "access_token": "8265eee1-1309-4481-a734-24a2a4f19299",

  "expires_in": 43189,

TOKEN=8265eee1-1309-4481-a734-24a2a4f19299

現在,我們已經擷取到了通路令牌,可以開始通路實際的API了。

首先在沒有擷取到通路令牌時,嘗試通路API,将會失敗:

curl 'http://localhost:8765/api/product/123' -s | jq .

  "error": "unauthorized",

  "error_description": "Full authentication is required to access this resource"

OK,這符合我們的預期。

接着,我們嘗試使用一個無效的通路令牌,仍然會失敗:

curl 'http://localhost:8765/api/product/123' \

 -H  "Authorization: Bearer invalid-access-token" -s | jq .

  "error": "access_denied",

  "error_description": "Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it."

再一次如期地拒絕了通路請求。

現在,我們嘗試使用許可流程傳回的通路令牌,執行正确的請求:

 -H  "Authorization: Bearer $TOKEN" -s | jq .

  "productId": 123,

  "name": "name",

  "weight": 123,

  "recommendations": [...],

  "reviews": [... ]

OK,這次工作正常了!

可以檢視一下api-service(product-api-service)輸出的日志記錄。

2015-04-23 18:39:59.014  INFO 79321 --- [ XNIO-2 task-20] o.s.c.s.o.r.UserInfoTokenServices        : Getting user info from: http://localhost:9999/uaa/user

2015-04-23 18:39:59.030  INFO 79321 --- [ctApiService-10] s.c.m.a.p.service.ProductApiService      : ProductApi: User=user, Auth=Bearer a0f91d9e-00a6-4b61-a59f-9a084936e474, called with productId=123

2015-04-23 18:39:59.381  INFO 79321 --- [ctApiService-10] s.c.m.a.p.service.ProductApiService      : GetProductComposite http-status: 200

我們看到API 聯系Authorization Server,擷取使用者資訊,并在log中列印出使用者名和通路令牌。

最後,我們嘗試使通路令牌失效,模拟它過期了。可以通過重新開機auth-server(僅在記憶體中存儲了該資訊)來進行模拟,接着再次執行前面的請求:

如我們的預期一樣,之前可以接受的通路令牌現在被拒絕了。

多謝開源項目spring-cloud-security和spring-security-auth,我們可以基于OAuth 2.0輕松設定安全API。然後,請記住我們使用的Authorization Server僅适用于開發和測試環境。

在随後的文章中,将使用ELK 技術棧(Elasticsearch、LogStash和Kibana)實作集中的log管理。

英文原文連結:

建構微服務(Blog Series - Building Microservices)

http://callistaenterprise.se/blogg/teknik/2015/05/20/blog-series-building-microservices/

繼續閱讀