1. 簡介
Spring Cloud Gateway 是一個庫,允許我們基於 Spring Boot 快速創建輕量級 API 網關,我們之前在之前的文章中已經涵蓋了它。
現在,我們將演示如何在上面快速實現 OAuth 2.0 模式。
2. OAuth 2.0 快速回顧
OAuth 2.0 標準是一個在互聯網上廣泛使用的成熟標準,作為一種安全機制,允許用户和應用程序安全地訪問資源。
雖然本文檔無法對該標準進行詳細描述,但讓我們先快速回顧一下幾個關鍵術語:
- Resource:只能由授權客户端檢索的信息類型
- Client:通過 REST API 消費資源的一個應用程序
- Resource Server:負責向授權客户端提供資源的服務器
- Resource Owner:擁有資源的實體(人類或應用程序),最終負責授予客户端對其訪問權限的實體
- Token:客户端獲取併發送給資源服務器作為請求的一部分,用於身份驗證的信息
- Identity Provider (IdP):驗證用户憑據並向客户端頒發訪問令牌的服務
- Authentication Flow:客户端獲取有效令牌所必須經過的步驟
對於該標準的全面描述,一個好的起點是 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 身份提供程序。唯一需要更改的是添加一個新的客户端應用程序和幾個用户用於測試。
為了使事情變得更有趣,我們的後端服務將根據請求中關聯的用户返回不同的報價價格。擁有“gold”角色的用户將獲得更低的價格,而其他人將獲得標準價格(生活本來就是不公平的,畢竟 ;^)).
我們將使用 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 構建和運行。我們只需要添加 `-Djava.version=11` 到 Maven 命令。
接下來,我們將替換 src/main/resources/baeldung-domain.json 為 這個版本。修改後的版本包含原始版本中可用的相同配置,以及一個額外的客户端應用程序 (quotes-client),兩個用户組 (golden_ 和 silver_customers),以及兩個角色 (gold 和 silver)。
現在,我們可以使用 spring-boot:run Maven 插件啓動服務器:
$ mvn spring-boot:run
... 許多、許多日誌消息已省略
2022-01-16 10:23:20.318
INFO 8108 --- [主線程] c.baeldung.auth.AuthorizationServerApp : 啓動 AuthorizationServerApp,持續時間為 23.815 秒(JVM 運行 24.488 秒)
2022-01-16 10:23:20.334
INFO 8108 --- [主線程] c.baeldung.auth.AuthorizationServerApp : 嵌入式 Keycloak 已啓動:http://localhost:8083/auth 以使用 keycloak
一旦服務器啓動,我們就可以通過將瀏覽器指向 http://localhost:8083/auth/admin/master/console/#/realms/baeldung 來訪問它。一旦我們使用管理員憑據 (bael-admin/pass) 登錄,我們就將獲得 Realm 的管理屏幕:
為了完成 IDP 設置,讓我們添加一些用户。第一個是 Maxwell Smart,他是 golden_customer 組的成員。第二個是 John Snow,我們將不將其添加到任何組中。
使用提供的配置,golden_customers 組的成員將自動假設 gold 角色。
5.2. 後端服務
quotes 後端需要常規 Spring Boot Reactive MVC 依賴項,以及 資源服務器啓動器依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
請注意,我們故意省略了依賴項的版本。這是 SpringBoot 推薦做法,當使用 SpringBoot 的 parent POM 或相應的 BOM 在依賴管理部分時。
在主應用程序類中,我們必須使用 @EnableWebFluxSecurity 啓用 WebFlux 安全性:
@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 ,該 Bean 從自定義字段中提取這些角色,這些字段由 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 包含用於此目的的配置文件:
$ 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 應用程序作為資源服務器與常規資源服務無異。因此,我們必須添加與後端服務相同的啓動器依賴項:
<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:
@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,使用具有所需設置的特定配置文件:
$ mvn spring-boot:run -Pgateway-as-resource-server
6.1. 測試資源服務器
現在我們已經擁有了拼圖的所有碎片,讓我們把它們組合起來。首先,我們必須確保 Keycloak、quotes 後端和網關都在運行。
接下來,我們需要從 Keycloak 獲取訪問令牌。 在這種情況下,獲取訪問令牌的最簡單方法是使用密碼授權流程(也稱為“擁有者”),這意味着向 Keycloak 發送 POST 請求,其中包含用户名/密碼、客户端 ID 和客户端密鑰,以及 quotes 客户端應用程序:
$ 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...'
這會產生 JSON 格式的報價:
{
"symbol":"BAEL",
"price":12.0
}
讓我們重複此過程,這次使用 Maxwell Smart 的訪問令牌:
{
"symbol":"BAEL",
"price":10.0
}
我們看到價格較低,這意味着後端能夠正確地識別關聯的用户。 還可以檢查未身份驗證的請求不會傳遞到後端,使用沒有 Authorization 標頭 $ curl http://localhost:8086/quotes/BAEL 的 curl 請求。
檢查網關日誌,我們看到請求轉發消息中沒有任何消息。 這表明響應是在網關上生成的。
7. Spring Gateway 作為 OAuth 2.0 客户端
對於啓動類,我們將使用我們為資源服務器版本已經存在的相同類。我們使用此方法強調所有安全行為來自可用的庫和屬性。
實際上,將兩個版本進行比較時,唯一明顯的區別在於配置屬性。在這裏,我們需要使用 issuer-uri 屬性或各種端點(授權、令牌和反思)的單獨設置來配置提供方詳情。
我們還需要定義應用程序客户端註冊詳細信息,其中包括請求的範圍。這些範圍告知 IdP 將通過反思機制提供哪些信息項:
... 其他屬性已省略
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:
... 其他路由定義已省略
7.1. 測試 Spring Gateway 作為 OAuth 2.0 客户端
對於測試設置,我們還需要確保我們有項目的三個組件正在運行。但是這一次,我們將使用包含所需屬性以使其作為 OAuth 2.0 客户端的另一個 Spring 配置文件運行網關。該項目樣子的 POM 包含一個允許我們通過啓用此配置文件來啓動它的配置文件:
$ 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 實施它們。