1. 引言
Spring Cloud Gateway 是一個庫,允許我們基於 Spring Boot 快速創建輕量級 API 網關,我們之前在之前的文章中已經涵蓋過。
現在,我們將演示如何在上面快速實現 OAuth 2.0 模式。
2. OAuth 2.0 快速回顧
OAuth 2.0 標準是一個在互聯網上廣泛使用的成熟標準,它作為一種安全機制,允許用户和應用程序安全地訪問資源。
雖然本文檔無法對該標準進行詳細描述,但讓我們先對幾個關鍵術語進行快速回顧:
- 資源:只能由授權客户端檢索的信息類型
- 客户端:通常通過 REST API 消費資源的應用
- 資源服務器:負責向授權客户端提供資源的服務器
- 資源所有者:擁有資源的實體(人類或應用程序),最終負責將訪問權限授予客户端
- 令牌:客户端獲取並作為請求的一部分發送給資源服務器以進行身份驗證的信息
- 身份提供者 (IdP): 驗證用户憑據並向客户端頒發訪問令牌的服務
- 身份驗證流程: 客户端必須執行的步驟序列以獲取有效的令牌。
對於該標準的全面描述,一個好的起點是 Auth0 的 關於該主題的文檔。
3. OAuth 2.0 模式
Spring Cloud Gateway 主要用於以下角色之一:
- OAuth 客户端
- OAuth 資源服務器
讓我們更詳細地討論這些情況。
3.1. Spring Cloud Gateway 作為 OAuth 2.0 客户端
在該場景中,任何未經過身份驗證的傳入請求將啓動授權碼流程。一旦網關獲取到令牌,它將被用於向後端服務發送請求:
這種模式在實際應用中的一個良好示例是社交網絡聚合應用:對於每個支持的網絡,網關將充當 OAuth 2.0 客户端。
因此,前端(通常是使用 Angular、React 或類似 UI 框架構建的 SPA 應用)可以在用户代表下無縫地訪問這些網絡上的數據,而無需用户泄露其憑據。
3.2. Spring Cloud Gateway 作為 OAuth 2.0 資源服務器
在此,網關充當守門人,強制要求每個請求在將它們發送到後端服務之前必須具有有效的訪問令牌。 此外,它還可以檢查令牌是否具有根據關聯的範圍訪問給定資源的適當權限:
需要注意的是,這種權限檢查主要在粗粒度級別上進行。 精細的訪問控制(例如,對象/字段級別的權限)通常在後端使用領域邏輯中實現。
在這種模式中需要考慮的是,後端服務如何驗證和授權任何轉發的請求。 主要有兩種情況:
- 令牌傳播:API 網關將接收到的令牌原封不動地轉發到後端
- 令牌替換:API 網關在將請求發送到後端之前,用另一個令牌替換傳入的令牌。
在本教程中,我們將僅涵蓋令牌傳播情況,因為它是最常見的場景。 另一種情況也是可行的,但需要額外的設置和編碼,這會分散我們對此處想要展示的主要重點。
4. 示例項目概述
為了演示如何使用 Spring Gateway 以及我們之前描述的 OAuth 模式,我們來構建一個示例項目,該項目暴露一個單一端點:/quotes/{symbol}。訪問此端點需要由配置的身份提供程序頒發的有效訪問令牌。
在我們的案例中,我們將使用嵌入式 Keycloak 身份提供程序。唯一的更改是添加一個新的客户端應用程序和幾個用户用於測試。
為了使事情變得更加有趣,我們的後端服務將根據請求中關聯的用户返回不同的報價價格。擁有“黃金”角色的用户將獲得更低的價格,而其他人將獲得標準價格(生活本來就是這樣,畢竟;^))。
我們將使用 Spring Cloud Gateway 作為前端,通過修改幾行配置,即可將該服務的角色從 OAuth 客户端切換為資源服務器。
5. 項目設置
5.1. Keycloak IdP
我們將用於本教程的嵌入式 Keycloak 只是一個普通的 SpringBoot 應用,我們可以從 GitHub 克隆並使用 Maven 構建:
$ git clone https://github.com/Baeldung/spring-security-oauth
$ cd oauth-rest/oauth-authorization/server
$ mvn install注意:本項目的目標是 Java 13+,但也能很好地構建和運行 Java 11。只需在 Maven 命令中添加 -Djava.version=11。
接下來,我們將替換 src/main/resources/baeldung-domain.json 為 這個版本。修改後的版本與原始版本具有相同的配置,幷包含一個額外的客户端應用程序 (quotes-client)、兩個用户組 (golden_ 和 silver_customers) 以及兩個角色 (gold 和 silver)。
現在,我們可以使用 spring-boot:run Maven 插件啓動服務器:
$ mvn spring-boot:run
... many, many log messages omitted
2022-01-16 10:23:20.318
INFO 8108 --- [ main] c.baeldung.auth.AuthorizationServerApp : Started AuthorizationServerApp in 23.815 seconds (JVM running for 24.488)
2022-01-16 10:23:20.334
INFO 8108 --- [ main] c.baeldung.auth.AuthorizationServerApp : Embedded Keycloak started: http://localhost:8083/auth to use keycloak服務器啓動後,可以通過將瀏覽器指向 http://localhost:8083/auth/admin/master/console/#/realms/baeldung 進行訪問。 登錄後使用管理員憑據 (bael-admin/pass),我們將獲得領域管理屏幕。
為了完成 IdP 設置,我們添加一些用户。 第一個用户是 Maxwell Smart,他是 golden_customer 組的成員。 第二個用户是 John Snow,我們將不將其添加到任何組中。
golden_customers group will automatically assume the gold role.">使用提供的配置,golden_customers 組的成員將自動承擔 gold 角色。
5.2. 後端服務
該報價後端需要依賴 Spring Boot Reactive MVC 規範的依賴項,以及 資源服務器啓動器依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
請注意,我們故意省略了依賴項的版本號。這在當使用 SpringBoot 的父 POM 或依賴管理部分中的相應 BOM 時,是被推薦的做法。
在主應用程序類中,必須使用 啓用 Web Flux 安全性:
@SpringBootApplication
@EnableWebFluxSecurity
public class QuotesApplication {
public static void main(String[] args) {
SpringApplication.run(QuotesApplication.class);
}
}該端點實現使用提供的 BearerAuthenticationToken 來檢查當前用户是否具有 gold 角色:
@RestController
public class QuoteApi {
private static final GrantedAuthority GOLD_CUSTOMER = new SimpleGrantedAuthority("gold");
@GetMapping("/quotes/{symbol}")
public Mono<Quote> getQuote(@PathVariable("symbol") String symbol,
BearerTokenAuthentication auth ) {
Quote q = new Quote();
q.setSymbol(symbol);
if ( auth.getAuthorities().contains(GOLD_CUSTOMER)) {
q.setPrice(10.0);
}
else {
q.setPrice(12.0);
}
return Mono.just(q);
}
}
現在,Spring 如何獲取用户角色?畢竟這與諸如 scopes 或 email 這樣標準聲明不同。事實上,這裏沒有魔法:我們必須提供一個自定義的 ReactiveOpaqueTokenIntrospection,它從 Keycloak 返回的自定義字段中提取這些角色。這個 Bean,可以在線找到,基本上與 Spring 的 文檔 上述主題相同,僅有少量針對我們自定義字段的修改。
我們還需要提供訪問身份提供程序的配置屬性:
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=quotes-client
spring.security.oauth2.resourceserver.opaquetoken.client-secret=<CLIENT SECRET>
最後,要運行我們的應用程序,我們可以選擇在 IDE 中導入它或從 Maven 中運行它。項目的 POM 文件包含一個用於此目的的 profile:
$ mvn spring-boot:run -Pquotes-application該應用程序現在已準備好在 http://localhost:8085/quotes 處處理請求。我們可以使用 curl 檢查其響應情況:
$ curl -v http://localhost:8085/quotes/BAEL正如預期的那樣,由於未發送任何Authorization 標頭,我們收到一個401 Unauthorized 響應。
6. Spring Gateway 作為 OAuth 2.0 資源服務器
保護一個作為資源服務器的 Spring Cloud Gateway 應用程序,與普通資源服務沒有本質區別。 因此,我們必須添加與後端服務相同的 starter 依賴,這也不足為奇。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
因此,我們還需要在啓動類中添加 @EnableWebFluxSecurity</em/>:
@SpringBootApplication
@EnableWebFluxSecurity
public class ResourceServerGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceServerGatewayApplication.class,args);
}
}
安全相關的配置屬性與後端相同。
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
client-id: quotes-client
client-secret: <CLIENT SECRET> 接下來,我們以與我們在 Spring Cloud Gateway 設置文章中相同的方式添加路由聲明:
... other properties omitted
cloud:
gateway:
routes:
- id: quotes
uri: http://localhost:8085
predicates:
- Path=/quotes/**
請注意,除了安全依賴和屬性之外,我們並未對網關本身進行任何修改。
要運行網關應用程序,我們將使用 spring-boot:run,並使用具有所需配置的特定 profile:
$ mvn spring-boot:run -Pgateway-as-resource-server6.1. 測試資源服務器
現在我們已經收集了所有拼圖的碎片,讓我們將它們組合起來。首先,我們需要確保 Keycloak、引用後端和網關都已運行。
接下來,我們需要從 Keycloak 獲取訪問令牌。 最直接的方法是使用密碼授權流程(也稱為“資源所有者”)。這意味着向 Keycloak 發送 POST 請求,其中包含一個用户的用户名/密碼,以及用於引用客户端應用程序的客户端 ID 和密鑰:
$ curl -L -X POST \
'http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=quotes-client' \
--data-urlencode 'client_secret=0e082231-a70d-48e8-b8a5-fbfb743041b6' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'scope=email roles profile' \
--data-urlencode 'username=john.snow' \
--data-urlencode 'password=1234'
響應將是一個 JSON 對象,其中包含訪問令牌,以及其他值:
{
"access_token": "...omitted",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "...omitted",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "7fd04839-fab1-46a7-a179-a2705dab8c6b",
"scope": "profile email"
}
我們現在可以使用返回的訪問令牌來訪問 /quotes API:
$ curl --location --request GET 'http://localhost:8086/quotes/BAEL' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer xxxx...'<p>哪個會產生一個 JSON 格式的報價?</p>
{
"symbol":"BAEL",
"price":12.0
}讓我們重複這個過程,這次使用 Maxwell Smart 的訪問令牌:
{
"symbol":"BAEL",
"price":10.0
}我們注意到我們擁有更低的價格,這意味着後端能夠正確地識別出相關的用户。 此外,我們還可以確認未認證的請求不會被傳播到後端,通過使用不帶Authorization 標頭
$ curl http://localhost:8086/quotes/BAEL檢查網關日誌,我們發現沒有與請求轉發過程相關的消息。 這表明響應是在網關生成的。
7. Spring Gateway 作為 OAuth 2.0 客户端
對於啓動類,我們將使用與資源服務器版本相同的版本。 這旨在強調所有安全行為都來自可用的庫和屬性。
實際上,將兩者進行比較時,唯一明顯的差異在於配置屬性。在這裏,我們需要使用 issuer-uri 屬性或各個端點(授權、令牌和內省)的單獨設置來配置提供者詳情。
我們還需要定義應用程序客户端註冊詳情,其中包括請求的範圍。這些範圍告知 IdP 通過內省機制將哪些信息項可用。
... other propeties omitted
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://localhost:8083/auth/realms/baeldung
registration:
quotes-client:
provider: keycloak
client-id: quotes-client
client-secret: <CLIENT SECRET>
scope:
- email
- profile
- roles
最後,在路由定義部分,還有一項重要的變更。任何需要傳播訪問令牌的路由都必須添加 TokenRelay 過濾器:
spring:
cloud:
gateway:
routes:
- id: quotes
uri: http://localhost:8085
predicates:
- Path=/quotes/**
filters:
- TokenRelay=
如果希望所有路由都啓動授權流程,我們可以將 TokenRelay 過濾器添加到 default-filters 部分:
spring:
cloud:
gateway:
default-filters:
- TokenRelay=
routes:
... other routes definition omitted7.1. 測試 Spring Gateway 作為 OAuth 2.0 客户端
為了測試環境,我們還需要確保三個項目組件都在運行。 這一次,我們將使用包含所需屬性的不同 Spring 配置文件來運行 Gateway,使其作為 OAuth 2.0 客户端。 該示例項目的 POM 包含一個配置文件,允許我們啓用此配置運行 Gateway。
$ mvn spring-boot:run -Pgateway-as-oauth-client網關運行後,我們可以通過將瀏覽器指向 http://localhost:8087/quotes/BAEL 進行測試。如果一切正常,我們將會被重定向到 IdP 的登錄頁面。
由於我們使用了 Maxwell Smart 的憑據,我們再次獲得了一份價格較低的報價。
為了總結我們的測試,我們將使用匿名/隱身瀏覽器窗口,並使用 John Snow 的憑據測試此端點。 此時我們獲得了正常的報價價格。
8. 結論
在本文中,我們探討了 OAuth 2.0 安全模式及其使用 Spring Cloud Gateway 實現的方法。