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
此時,任何到達 Zuul 上的請求,即 localhost:8080/oauth/**,都將被路由到端口 8081 上的身份驗證服務。任何到 localhost:8080/spring-security-oauth-resource/** 的請求都將被路由到端口 8082 上的資源服務器。
5. 確保 Zuul 外部流量路徑安全
即使我們的 Zuul 邊緣服務現在已正確路由請求,但它沒有進行任何授權檢查。位於 /oauth/* 之後,授權服務器會為每次成功的身份驗證創建一個 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。這些服務會自行驗證 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 保護的路由配置,擴展 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 強制所有其他調用應包含訪問令牌。
6. 配置用於 JWT 驗證的密鑰
現在配置已就位,所有路由到 /oauth/** 的請求將被匿名允許通過,而所有其他請求都需要身份驗證。
不過,我們缺少一件事情,那就是用於驗證 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. 測試邊緣服務
7.1. 獲取訪問令牌
現在,讓我們測試我們的 Zuul 邊緣服務如何運行——使用一些 curl 命令。
首先,我們將使用 Authorization Server 獲取新的 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…” 標頭字段。 這用於告知 Authorization Server 連接的客户端。
它對客户端(在本例中為 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. 測試資源服務器請求
我們可以使用 Authorization Server 獲取的 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,它允許我們在服務邊界上進行授權。