Spring Security 中的代幣交換指南
1. 引言
Spring Security 6.3 為 OAuth 2.0 令牌交換授權( RFC 8693 )提供了完善的支援。它允許我們在保留原始最終用戶身份的情況下,將現有的存取權杖交換為具有不同受眾的新令牌。這在微服務和伺服器間通訊中尤其有用,因為在這些場景中,令牌必須針對不同的受眾進行作用域劃分。本文將解釋什麼是令牌交換,以及如何在 Spring Boot 應用程式中設定它——包括資源伺服器(充當 OAuth2 用戶端)和授權伺服器。
2. 什麼是 OAuth 2.0 令牌交換?
OAuth 2.0 令牌交換提供了一種標準化的機制,在保留原始最終使用者身分的前提下,將一個存取權杖交換為另一個存取權杖。這使得應用程式能夠獲取一個面向不同受眾或資源的令牌,而無需用戶重新進行身份驗證。假設有這樣一個場景:名為user-service資源伺服器(用於存取使用者資訊的 API)收到一個使用 bearer 令牌進行身份驗證的傳入請求。該令牌有效且作用域正確,適用於user-service.假設令牌必須具有user-service的受眾( aud聲明):
{
"aud": "user-service"
}
在請求處理過程中, user-service需要代表同一使用者呼叫另一個受保護的內部 API— message-service 。然而,原始存取權杖無法用於此目的,因為其aud (受眾)聲明僅允許存取user-service 。我們希望保留原始請求中使用者的身分。 OAuth 2.0 令牌交換透過允許user-service將傳入的存取權杖提交給授權伺服器並交換一個新的存取權杖來解決這個問題。新令牌的作用域專門針對message-service 。為了取得此令牌, user-service暫時充當 OAuth 2.0 用戶端。它將現有令牌交換為保留原始最終使用者身分和授權上下文的新令牌。此流程通常稱為模擬。它無需任何額外的用戶互動即可實現安全的服務間通訊。
3. Maven 依賴項
首先,讓我們將spring-boot-starter-oauth2-resource-server依賴項匯入pom.xml中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>3.5.11</version>
</dependency>
此外,我們還需要spring-boot-starter-oauth2-client依賴項,因為令牌交換在請求新令牌時會將資源伺服器視為 OAuth 2.0 用戶端:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>3.5.11</version>
</dependency>
最後,要在授權伺服器端啟用令牌交換,我們需要 Spring Authorization Server 1.3 或更高版本。此版本引入了對令牌交換授權的內建支援。讓我們加入spring-boot-starter-oauth2-authorization-server 依賴關係:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<version>3.5.11</version>
</dependency>
4. 設定資源伺服器
在令牌交換流程中,資源伺服器扮演著兩個截然不同的角色。首先,它作為資源伺服器,負責驗證傳入的存取權杖並保護自身的 API。其次,它作為 OAuth 2.0 用戶端,在代表使用者呼叫下游服務時,會使用傳入的存取權令牌請求新的令牌。
4.1. 資源伺服器作為伺服器
我們將先把user-service配置成標準的 OAuth 2.0 資源伺服器。在這個角色中, user-service會保護它自己的 API,並驗證授權伺服器所發出的傳入存取權杖。讓我們建立一個UserController類,並在/user/message中公開一個受保護的端點:
@RestController
public class UserController {
@GetMapping(value = "/user/message")
public String message() {
return "baeldung";
}
}
使用 JWT 時,資源伺服器需要知道從哪裡取得令牌頒發者的元資料。我們可以使用issuer-uri屬性來配置這一點:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9001
audiences: user-service
在我們的範例中,提交給user-service存取權令牌必須包含值為user-service aud (受眾)聲明。至此,應用程式已完全配置為資源伺服器,可以接受並驗證授權伺服器所發出的存取權杖。
4.2. 資源伺服器作為客戶端
接下來,我們需要將user-service配置為 OAuth 2.0 用戶端。在此角色下,資源伺服器在代表已認證使用者呼叫下游服務時,會將傳入的存取權杖交換為新令牌。為了啟用此功能,我們需要定義一個使用 OAuth 2.0 令牌交換授權類型的用戶端註冊。我們可以在application-userservice.yml中進行設定:
spring:
security:
oauth2:
client:
registration:
my-message-service:
provider: my-auth-server
client-id: "message-service"
client-secret: "token"
authorization-grant-type: "urn:ietf:params:oauth:grant-type:token-exchange"
client-authentication-method:
- "client_secret_basic"
scope:
- message.read
client-name: my-message-service
provider:
my-auth-server:
issuer-uri: http://localhost:9001
此組態註冊一個與授權伺服器通訊的 OAuth 2.0 用戶端。 `authorization **authorization-grant-type屬性指定此用戶端使用令牌交換授權。這允許它基於現有存取權令牌來獲取新的存取權杖**。 `requested scope` 屬性決定了授予下游服務的存取等級。
4.3. 設定 Spring Security 以實現令牌交換
若要使用代幣交換,我們還需要啟用 Spring Security 中的新授權類型。我們可以透過發佈TokenExchangeOAuth2AuthorizedClientProvider bean 來實現這一點。我們需要一個OAuth2AuthorizedClientManager來以程式方式處理令牌交換邏輯。此管理器負責協調獲取授權客戶端的過程。我們將配置它以使用我們的TokenExchangeOAuth2AuthorizedClientProvider 。讓我們建立一個TokenExchangeConfig類別來定義這些 bean:
@Configuration
public class TokenExchangeConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider =
new TokenExchangeOAuth2AuthorizedClientProvider();
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.provider(tokenExchangeAuthorizedClientProvider)
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
此外,由於 Spring Security 6.3 支援RestClient ,我們將定義一個自訂的OAuth2AccessTokenResponseClient 。它使用RestClientTokenExchangeTokenResponseClient來處理實際的 HTTP 代幣交換請求:
@Bean
public OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient() {
return new RestClientTokenExchangeTokenResponseClient();
}
完成此配置後,我們可以在一個資源伺服器上取得存取權杖。然後,我們將其用作 Bearer 令牌,向另一個資源伺服器發出受保護資源請求。
4.4. 完成UserController
現在我們可以更新UserController ,以程式設計方式執行令牌交換。我們需要注入OAuth2AuthorizedClientManager類別。然後,我們將建立一個OAuth2AuthorizeRequest ,指定要使用的客戶端註冊(在application-userservice.yml中定義)。管理器會自動偵測傳入的令牌。它會將令牌與授權伺服器交換,並傳回一個包含針對message-service新存取權杖的OAuth2AuthorizedClient 。接下來,讓我們實作 UserController:
@GetMapping("/user/message")
public String message(JwtAuthenticationToken jwtAuthentication) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("my-message-service")
.principal(jwtAuthentication)
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
assert authorizedClient != null;
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
if (accessToken == null) {
return "token exchange resource server";
}
RestClient.ResponseSpec responseSpec = restClient.get().uri(TARGET_RESOURCE_SERVER_URL)
.headers(headers -> headers.setBearerAuth(accessToken.getTokenValue()))
.retrieve();
ResponseEntity<String> responseEntity = responseSpec.toEntity(String.class);
return responseEntity.getBody();
}
message()方法接受目前使用者透過JwtAuthenticationToken進行的身份驗證。然後,我們建立一個針對my-message-service註冊的OAuth2AuthorizeRequest使用此請求將原始令牌交換為適用於下游服務的新令牌。最後,我們使用RestClient呼叫目標 API authorizedClientManager並使用交換後的存取權杖。
5. 設定授權伺服器
為了完成令牌交換流程,我們還需要設定授權伺服器。授權伺服器除了頒發存取權杖外,還必須接受令牌交換請求。它會頒發具有更新後的受眾或範圍值的新令牌。
5.1. 代幣兌換授權支持
Spring Authorization Server 1.3 新增了對 OAuth 2.0 Token Exchange 授權的內建支援。這項支援使我們能夠在無需編寫自訂擴充功能的情況下實現該流程。我們可以透過 OpenID Provider Metadata 端點驗證此功能: http://localhost:9001/.well-known/openid-configuration如果grant_types_supported欄位包含urn:ietf:params:oauth:grant-type:token-exchange exchange ,則表示伺服器支援 RFC 8693 中定義的令牌交換請求。
5.2. 註冊代幣兌換客戶端
要啟用令牌交換,我們需要向授權伺服器註冊一個客戶端。該用戶端被明確授權使用「 urn:ietf:params:oauth:grant-type:token-exchange 」授權類型。在 Spring 授權伺服器中,我們在屬性中進行設定:
spring:
security:
oauth2:
authorizationserver:
client:
message-service:
registration:
client-id: "message-service"
client-secret: "{noop}token"
client-authentication-methods:
- "client_secret_basic"
authorization-grant-types:
- "urn:ietf:params:oauth:grant-type:token-exchange"
scopes:
- "openid"
- "message:read"
此配置允許客戶端使用客戶端憑證進行身份驗證,並使用令牌交換授權類型請求存取權杖。授權伺服器配置為支援令牌交換後,端到端流程即告完成。
6. 端到端流程測試
在本節中,我們將測試完整的 OAuth 2.0 流程。我們將模擬一個真實場景:使用者透過瀏覽器登入客戶端應用程式。客戶端隨後呼叫user-service ,用戶服務再執行令牌交換,從而與message-service進行安全通訊。為了演示這一點,我們創建了一個簡單的客戶端應用程式作為前端。
6.1 客戶端應用程式安裝
讓我們建立一個ClientController類別。它將“ /api/user/message ”端點作為受保護的資源公開,並使用當前使用者的存取權令牌呼叫user-service :
@RestController
public class ClientController {
private static final String TARGET_RESOURCE_SERVER_URL = "http://localhost:8081/user/message";
@GetMapping("/api/user/message")
public String userMessage(@RegisteredOAuth2AuthorizedClient(registrationId = "messaging-client-oidc")
OAuth2AuthorizedClient oauth2AuthorizedClient) {
RestClient.ResponseSpec responseSpec = restClient.get().uri(TARGET_RESOURCE_SERVER_URL)
.headers(headers -> headers.setBearerAuth(oauth2AuthorizedClient.getAccessToken().getTokenValue()))
.retrieve();
String messageFromResourceServer = responseSpec.toEntity(String.class).getBody();
return "<html><body><title>Token Exchange</title><p>Token Exchange Client!</p></br><p>" +
"The resource server: <strong>" + messageFromResourceServer + "</strong></p></body></html>";
}
}
@RegisteredOAuth2AuthorizedClient註解會自動注入與messaging-client-oidc註冊關聯的OAuth2AuthorizedClient客戶端。該用戶端保存著使用者登入後所取得的存取令牌。
6.2 運行測試
首先,我們打開瀏覽器並訪問客戶端應用程式的主頁(假設它運行在 8080 連接埠)。我們將被重新導向到授權伺服器的登入頁面:
然後,我們使用使用者憑證( baeldung / password )登入。登入成功後,我們可以看到使用者的主體和授權方。這確認客戶端已透過授權碼流程成功驗證了使用者身分:
最後,我們點擊頁面上提供的連結訪問「 /api/user/message 」。這將呼叫userMessage()方法,該方法使用注入的存取權杖呼叫user-service 。 user-service驗證傳入的令牌。然後,它會向授權伺服器發送請求,將令牌交換為message-service作用域的新令牌。最後,它使用交換後的令牌來獲取訊息。以下是交換後的令牌:
{
"sub": "baeldung",
"aud": "message-service",
"scope": [
"message.read"
],
// ...
}
然後瀏覽器顯示來自資源伺服器的回應,確認令牌交換已成功完成。
7. 結論
本文探討了 RFC 8693 中定義的 OAuth 2.0 令牌交換。它提供了一種標準化的機制,可以在保留原始最終使用者身分的情況下,將一個存取權杖交換為另一個存取權杖。我們詳細介紹了完整的設置,包括授權伺服器和資源伺服器。此外,我們也了解如何使用RestClient以程式方式觸發令牌交換並呼叫下游 API。這種方法確保了跨微服務邊界的身份和授權流程安全無縫。與往常一樣,原始碼已發佈在 GitHub 上。