1. 簡介
簡單來説,微服務架構允許我們將系統和我們的 API 分解為一組自包含的服務,這些服務可以獨立部署。
從持續部署和管理角度來看,這非常棒,但當涉及到 API 可用性時,可能會變得非常複雜。 隨着不同的端點需要管理,依賴的應用將需要管理 CORS(跨域資源共享)以及一組不同的端點。
Zuul 是一個邊緣服務,它允許我們將傳入的 HTTP 請求路由到多個後端微服務。 其中之一是為我們後端資源的消費者提供統一 API 的重要原因。
基本上,Zuul 允許我們通過在它們前面作為代理來統一所有服務。 它接收所有請求並將其路由到正確的服務。 對外部應用程序而言,我們的 API 看起來就像一個統一的 API 表面。
在本教程中,我們將討論如何使用它來實現這個目的,並結合 OAuth 2.0 和 JWT,作為保護我們 Web 服務的前線。 具體的,我們將使用 密碼授權流程 來獲取訪問受保護資源的訪問令牌。
請注意,我們僅使用密碼授權流程來探索一個簡單的場景;在生產場景中,大多數客户端更有可能使用授權流程。
2. 添加 Zuul Maven 依賴
讓我們首先添加 Zuul 到我們的項目。我們通過添加 spring-cloud-starter-netflix-zuul 構件來實現:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
3. 啓用 Zuul
該應用程序旨在通過 Zuul 路由,包含一個 OAuth 2.0 授權服務器,該服務器頒發訪問令牌,以及一個接受這些令牌的資源服務器。這兩個服務分別位於兩個獨立的端點上。
我們希望為這些服務的所有外部客户端提供一個單一的端點,並使用不同的路徑分支到不同的物理端點。為此,我們將 Zuul 作為邊緣服務引入。
為此,我們將創建一個新的 Spring Boot 應用程序,名為 GatewayApplication。然後,我們將簡單地使用 @EnableZuulProxy 註解裝飾該應用程序類,這將會啓動一個 Zuul 實例:
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4. 配置 Zuul 路由
在繼續之前,我們需要配置一些 Zuul 屬性。首先,我們需要配置 Zuul 監聽傳入連接的端口。這需要將其添加到 /src/main/resources/application.yml 文件中:
server:
port: 8080
現在我們來配置 Zuul 將要轉發的實際路由。為此,我們需要注意以下服務、它們的路徑以及它們監聽的端口。
授權服務器部署在:http://localhost:8081/spring-security-oauth-server/oauth
資源服務器部署在:http://localhost:8082/spring-security-oauth-resource
授權服務器是一個 OAuth 身份提供者。它的作用是向資源服務器提供授權令牌,進而提供一些受保護的端點。
授權服務器向客户端提供訪問令牌,客户端再利用該令牌向資源服務器執行請求,代表資源擁有者執行請求。 快速瀏覽一下 OAuth 術語 將有助於我們理解這些概念。
zuul:
routes:
spring-security-oauth-resource:
path: /spring-security-oauth-resource/**
url: http://localhost:8082/spring-security-oauth-resource
oauth:
path: /oauth/**
url: http://localhost:8081/spring-security-oauth-server/oauth
此時,任何通過 localhost:8080/oauth/** 訪問 Zuul 的請求都將被路由到端口 8081 上的授權服務。任何通過 localhost:8080/spring-security-oauth-resource/** 的請求都將被路由到端口 8082 上的資源服務器。
5. 安全 Zuul 外部流量路徑
儘管我們的 Zuul 邊緣服務現在已正確路由請求,但它沒有進行任何授權檢查。位於 /oauth/* 路徑下的授權服務器會為每次成功的身份驗證創建一個 JWT。自然地,該 JWT 對匿名用户開放。
資源服務器 – 位於 /spring-security-oauth-resource/** 路徑上,另一方面,始終應使用 JWT 訪問,以確保授權的客户端正在訪問受保護的資源。
首先,我們將配置 Zuul 以將 JWT 傳遞給位於其背後的服務。在本例中,這些服務本身需要驗證該令牌。
我們通過添加 sensitiveHeaders: Cookie,Set-Cookie 來實現。
這將完成 Zuul 的配置:
server:
port: 8080
zuul:
sensitiveHeaders: Cookie,Set-Cookie
routes:
spring-security-oauth-resource:
path: /spring-security-oauth-resource/**
url: http://localhost:8082/spring-security-oauth-resource
oauth:
path: /oauth/**
url: http://localhost:8081/spring-security-oauth-server/oauth
在處理完這些事項後,我們需要處理邊緣端點的授權。目前,Zuul 不會驗證 JWT 在傳遞給我們的下游服務之前。這些服務會自行驗證 JWT,但理想情況下,我們希望邊緣服務首先進行驗證,並在請求更深層次的架構傳播之前拒絕任何未授權請求。
讓我們使用 Spring Security 來確保在 Zuul 中進行授權檢查。
首先,我們需要將 Spring Security 依賴項引入到我們的項目中。我們想要 spring-security-oauth2 和 spring-security-jwt。
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
現在我們來編寫一個配置,用於通過擴展 ResourceServerConfigurerAdapter 來保護我們想要保護的路由。
@Configuration
@Configuration
@EnableResourceServer
public class GatewayConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**")
.permitAll()
.antMatchers("/**")
.authenticated();
}
}
GatewayConfiguration 類定義了 Spring Security 如何處理通過 Zuul 傳入的 HTTP 請求。在 configure 方法內部,我們首先使用 antMatchers 匹配最嚴格的路徑,然後允許匿名訪問通過 permitAll 允許。
所有進入 /oauth/** 路徑的請求都應該允許通過,而無需檢查任何授權令牌。這在邏輯上是有意義的,因為這是生成授權令牌的路徑。
接下來,我們匹配所有以 /** 結尾的路徑,並通過調用 authenticated 強制所有其他調用包含 Access Tokens。
6. 配置用於 JWT 驗證的密鑰
現在配置已就位,所有路由到 /oauth/**</em/> 路徑的請求將允許匿名通過,而所有其他請求都需要身份驗證。
還有一件需要處理的事,那就是用於驗證 JWT 的實際密鑰。要做到這一點,我們需要提供用於對 JWT 進行簽名(在本例中是對稱的)的密鑰。與其手動編寫配置代碼,不如使用 spring-security-oauth2-autoconfigure。
讓我們先將該 Artifact 添加到我們的項目:
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>接下來,我們需要在我們的 application.yaml 文件中添加幾行配置,以定義用於 JWT 簽名的密鑰:
security:
oauth2:
resource:
jwt:
key-value: 123
行 key-value: 123 設置了授權服務器用於對 JWT 進行簽名的對稱密鑰。此密鑰將由 spring-security-oauth2-autoconfigure 用於配置令牌解析。
請注意,在生產系統中,我們不應使用應用程序源代碼中指定的對稱密鑰。 這自然需要外部配置。
7. 測試邊緣服務
This section describes how to test the Edge Service. Testing is crucial to ensure the service is functioning correctly and meeting performance requirements.
7.1 Test Cases
The following test cases should be executed:
- Connectivity Test: Verify the Edge Service can establish and maintain connections to the backend services.
- Data Processing Test: Validate that data is processed correctly by the Edge Service, including data transformation and filtering.
- Latency Test: Measure the latency of requests processed by the Edge Service.
- Throughput Test: Assess the maximum throughput the Edge Service can handle.
- Error Handling Test: Confirm that the Edge Service handles errors gracefully and logs appropriate error messages.
- Scalability Test: Evaluate the Edge Service's ability to handle increasing workloads.
7.2 Test Environment
The following environment is required for testing:
- A dedicated test network isolated from production.
- The Edge Service deployed in a staging environment.
- Access to the backend services being integrated with the Edge Service.
7.3 Testing Tools
The following tools can be used for testing:
- Load Testing Tools: JMeter, Gatling
- Monitoring Tools: Prometheus, Grafana
- Debugging Tools: (Specific tools will depend on the Edge Service's architecture)
7.1. 獲取訪問令牌
現在讓我們測試 Zuul 邊緣服務的功能 – 使用一些 curl 命令。
首先,我們將查看如何從授權服務器獲取新的 JWT,使用 密碼授權類型。
在這裏,我們將 用户名和密碼用於獲取訪問令牌。 在這種情況下,我們將 ‘john‘ 作為用户名,以及 ‘123‘ 作為密碼:
curl -X POST \
http://localhost:8080/oauth/token \
-H 'Authorization: Basic Zm9vQ2xpZW50SWRQYXNzd29yZDpzZWNyZXQ=' \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password&password=123&username=john'
此調用會產生一個 JWT 令牌,我們可以將其用於對我們的資源服務器進行身份驗證的請求。
請注意 “Authorization: Basic…” 標頭字段。此字段用於告知身份驗證服務器哪個客户端正在與之通信。
它對客户端(在本例中為 cURL 請求)就像用户名和密碼對用户一樣。
{
"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
"token_type":"bearer",
"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
"expires_in":3599,
"scope":"foo read write",
"organization":"johnwKfc",
"jti":"8e2c56d3-3e2e-4140-b120-832783b7374b"
}
7.2. 測試資源服務器請求
我們可以使用從授權服務器檢索到的 JWT,現在執行一個查詢,該查詢針對資源服務器:
curl -X GET \
curl -X GET \
http:/localhost:8080/spring-security-oauth-resource/users/extra \
-H 'Accept: application/json, text/plain, */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Accept-Language: en-US,en;q=0.9' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV...' \
-H 'Cache-Control: no-cache' \
Zuul 邊緣服務現在將在路由到資源服務器之前驗證 JWT。
然後,它會從 JWT 中提取關鍵字段並進行更細粒度的授權檢查,然後再對請求作出響應:
{
"user_name":"john",
"scope":["foo","read","write"],
"organization":"johnwKfc",
"exp":1544584758,
"authorities":["ROLE_USER"],
"jti":"8e2c56d3-3e2e-4140-b120-832783b7374b",
"client_id":"fooClientIdPassword"
}
8. 多層安全保障
需要注意的是,JWT 在通過之前,首先會被 Zuul 邊緣服務進行驗證。如果 JWT 無效,則請求將在邊緣服務邊界處被拒絕。
如果 JWT 確實有效,則請求會被傳遞到下游。資源服務器隨後會再次驗證 JWT 並提取關鍵字段,例如用户權限範圍、組織(在本例中為自定義字段)和權限。它利用這些字段來確定用户可以執行的操作以及不能執行的操作。
為了明確起見,在許多架構中,我們可能不需要對 JWT 進行兩次驗證——這是一個您需要根據流量模式做出的決定。
例如,在某些生產項目中,單個資源服務器可能直接訪問,也可能通過代理訪問——並且我們可能需要在這兩個地方驗證令牌。而在其他項目中,流量可能僅通過代理進行,此時僅在代理處驗證令牌就足夠了。
9. 總結
正如我們所見,Zuul 提供了一種簡單、可配置的方法,用於抽象和定義服務路由。結合使用 Spring Security,它允許我們在服務邊界處進行請求授權。