Spring Security 6.4 中 RestClient 對 OAuth2 的支援

工程 | Steve Riesenberg | 2024 年 10 月 28 日 | ...

在 Spring Security 6.2 和 6.3 中,我們一直致力於穩步改進使用 OAuth2 Client 的應用程式配置。透過允許應用程式釋出在應用程式啟動期間自動包含在整體 OAuth2 Client 配置中的 bean,簡化了常見用例的配置。最近的改進包括:

  • 只需釋出一個型別為 OAuth2AuthorizedClientProvider(或 ReactiveOAuth2AuthorizedClientProvider)的 bean,即可啟用擴充套件授權型別。
  • 只需釋出一個或多個型別為 OAuth2AccessTokenResponseClient(或 ReactiveOAuth2AccessTokenResponseClient)的 bean,即可使用自定義引數擴充套件 OAuth 2.0 訪問令牌請求。
  • 如果尚未釋出,Spring Security 會自動釋出一個型別為 OAuth2AuthorizedClientManager(或 ReactiveOAuth2AuthorizedClientManager)的 bean,從而減少應用程式需要獲取訪問令牌時所需的樣板配置。

在 Spring Security 6.4 中,這一主題仍在繼續,此次改進重點關注 RestClient,這是 Spring Framework 6.1 中引入的一種新的 HTTP 客戶端。RestClient 提供了一個流暢的 API,與 WebClient 的 API 極其相似,但它是同步的,不依賴於響應式庫。這意味著配置應用程式使用 OAuth2 Client 發起受保護資源請求變得更加簡單,並且不需要任何額外的依賴。此外,還改進了使用 RestClient 的 Servlet 應用程式與使用 WebClient 的響應式應用程式之間的一致性,目標是使這兩個技術棧採用通用的配置模型。

接下來,我們詳細瞭解一下對 RestClient 的新支援以及 OAuth2 Client 的其他改進。

OAuth2 簡介

首先,我們先總結一下將要用到的 OAuth2 相關概念。

在 OAuth2 術語中,發起受保護資源請求意味著在出站請求的 Authorization 頭中包含一個訪問令牌,該請求傳送到一個資源伺服器。發起請求的應用程式被稱為客戶端,因為它發起這些出站請求。目標應用程式被稱為資源伺服器,因為它提供 API 來訪問屬於資源所有者(例如使用者)並由授權伺服器保護的資源(例如資料)。授權伺服器是一個系統,負責建立和管理代表授權許可的訪問令牌,它是根據客戶端代表資源所有者發出的請求(稱為 OAuth 2.0 訪問令牌請求)執行此操作的。

使用 RestClient 發起受保護資源請求

在簡要介紹之後,我們來了解如何在 Spring Security 6.4 中設定應用程式以使用 RestClient 發起受保護資源請求。前往 Spring Initializr 建立一個新的應用程式。如果您正在使用 Spring Boot 更新現有應用程式,則需要新增以下依賴項:

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

應用程式需要透過使用 ClientRegistrationRepository bean 配置至少一個 ClientRegistrationClientRegistration 類是 Spring Security 中的領域模型,它包含特定 OAuth2 客戶端的資料。每個客戶端都必須預先註冊到授權伺服器,此類別包含從授權伺服器獲取的詳細資訊,例如 clientIdclientSecret。它還包含我們希望使用的 authorizationGrantType,例如 authorization_codeclient_credentials,以及根據需要可選配置的幾個附加引數。

以下示例使用 Spring Boot 配置屬性配置了一個帶有單個 ClientRegistrationInMemoryClientRegistrationRepository bean:

application.yml:

spring:
  security:
    oauth2:
      client:
        registration:
          messaging-client:
            provider: spring
            client-id: client1
            client-secret: my-secret
            authorization-grant-type: authorization_code
            scope: message.read,message.write
        provider:
          spring:
            issuer-uri: https://:9000

上述配置允許 Spring Security 使用 本地授權伺服器 透過 authorization_code 授權獲取訪問令牌。

Spring Security 提供了 OAuth2AuthorizedClientManager 的實現,這是一個可用於獲取訪問令牌(例如 JWT)的元件。Spring Security 會自動將此元件的一個例項釋出為 bean,這意味著我們只需將其注入到我們的配置中,即可設定一個 RestClient 用於在應用程式中發起受保護資源請求。以下示例配置了一個最小化的 RestClient 並將其釋出為 bean:

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(RestClient.Builder builder, OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
			new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);

		return builder.requestInterceptor(requestInterceptor).build();
	}

}

現在我們可以在自己的應用程式中發起受保護資源請求了。以下示例演示瞭如何在 Spring MVC 控制器中執行此操作:

import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;

@RestController
public class MessagesController {

	private final RestClient restClient;

	public MessagesController(RestClient restClient) {
		this.restClient = restClient;
	}

	@GetMapping("/messages")
	public ResponseEntity<List<Message>> messages() {
		Message[] messages = this.restClient.get()
			.uri("https://:8090/messages")
			.attributes(clientRegistrationId("messaging-client"))
			.retrieve()
			.body(Message[].class);

		return ResponseEntity.ok(Arrays.asList(messages));
	}

	public record Message(String message) {
	}

}

上述示例使用了靜態方法,透過屬性向攔截器提供 registrationId "messaging-client"。提供的值與之前 yaml 配置中的值匹配,Spring Security 就是透過這種方式知道獲取訪問令牌時應使用哪個客戶端 id、secret、授權型別、scopes 以及其他資訊。

當然,這只是一個示例,您不僅限於在端點中簡單地返回結果。您可以在應用程式的任何部分執行此操作,例如負責發起受保護資源請求並將結果返回給應用程式的 @Service@Component

使用 RestClient 發起 OAuth 2.0 訪問令牌請求

在 Spring Security 6.4 之前,Servlet 技術棧的預設 HTTP 客戶端是 RestTemplate。由於 RestTemplateWebClient API 的差異,使用 RestTemplate 為 Servlet 應用程式自定義 OAuth 2.0 訪問令牌請求與自定義使用 WebClient 的響應式應用程式大相徑庭。

隨著 Spring Framework 6.1 中引入 RestClient,現在可以透過分別利用 RestClientWebClient 作為各自技術棧的基礎 HTTP 客戶端,使兩個技術棧採用非常相似的配置模型。如果需要,可以使用 RestClient.create(RestTemplate)RestTemplate 建立一個 RestClient,為使 Servlet 和響應式技術棧在通用配置模型上對齊提供了清晰的遷移路徑,這也是 Spring Security 7 的一個目標。

Spring Security 6.4 為此目的引入了 OAuth2AccessTokenResponseClient 的新實現。如果需要,您可以選擇在 Servlet 應用程式中對所有 OAuth2 Client 功能使用 RestClient 作為 HTTP 客戶端。以下示例演示了一個最小化配置,用於使用自定義的 RestClient 例項選擇啟用新的支援:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	private final RestClient restClient;

	@PostConstruct
	void initialize() {
		this.restClient = RestClient.builder()
			.messageConverters((messageConverters) -> {
				messageConverters.clear();
				messageConverters.add(new FormHttpMessageConverter());
				messageConverters.add(new OAuth2AccessTokenResponseHttpMessageConverter());
			})
			.defaultStatusHandler(new OAuth2ErrorResponseErrorHandler())
			// TODO: Customize the instance of RestClient as needed...
			.build();
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
		RestClientAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new RestClientAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
		RestClientRefreshTokenTokenResponseClient accessTokenResponseClient =
			new RestClientRefreshTokenTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		RestClientClientCredentialsTokenResponseClient accessTokenResponseClient =
			new RestClientClientCredentialsTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
		return (grantRequest) -> {
			throw new UnsupportedOperationException("The `password` grant type is not supported.");
		};
	}

	@Bean
	public OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
		RestClientJwtBearerTokenResponseClient accessTokenResponseClient =
			new RestClientJwtBearerTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
		RestClientTokenExchangeTokenResponseClient accessTokenResponseClient =
			new RestClientTokenExchangeTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

}

注意:新支援中沒有針對 password 授權型別的實現,因為對此授權型別的現有支援已被棄用,並計劃在 Spring Security 7 中移除。

覆蓋或省略預設引數

Spring Security 透過 OAuth2AccessTokenResponseClient(或 ReactiveOAuth2AccessTokenResponseClient)介面的實現支援多種授權型別。一個常見的需求是能夠自定義 OAuth 2.0 訪問令牌請求的引數,這在授權伺服器具有特定要求或提供支援規範中未涵蓋的功能時很典型。

在 Spring Security 6.3 及更早版本中,響應式應用程式無法覆蓋或省略 Spring Security 設定的引數值,這需要變通方法來為此類用例自定義應用程式。現在,透過 setParametersConverter() 自定義鉤子,響應式應用程式(使用 WebClient)和 Servlet 應用程式(使用 RestClient)都可以覆蓋引數。在這種情況下,重要的是要注意,所有特定於授權型別和預設引數將首先設定。您的自定義 parametersConverter 提供的任何引數將覆蓋現有引數。

除了覆蓋引數外,現在還可以省略可能被授權伺服器拒絕的引數。例如,當 ClientRegistration#clientAuthenticationMethod 設定為 private_key_jwt 時,我們可以使用包含生成的 JWT 的客戶端斷言提供客戶端認證。一些授權伺服器可能會拒絕同時包含 client_idclient_assertion 引數的請求。在這種情況下,由於 client_id 是 Spring Security 提供的預設引數,我們需要一種方法來根據我們將使用客戶端斷言提供客戶端認證的知識來省略此引數。

Spring Security 6.4 提供了使用 setParametersCustomizer() 自定義鉤子省略 OAuth 2.0 訪問令牌請求引數的功能。以下示例演示了在使用客戶端斷言進行客戶端認證並使用 client_credentials 授權時如何省略 client_id 引數:

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveClientCredentialsTokenResponseClient();
		accessTokenResponseClient.addParametersConverter(
			new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver()));
		accessTokenResponseClient.setParametersCustomizer((parameters) -> {
			if (parameters.containsKey(OAuth2ParameterNames.CLIENT_ASSERTION)) {
				parameters.remove(OAuth2ParameterNames.CLIENT_ID);
			}
		});

		return accessTokenResponseClient;
	}

	private Function<ClientRegistration, JWK> jwkResolver() {
		// ...
	}

}

提示:在使用 RestClientClientCredentialsTokenResponseClient(或其他授權型別的替代實現)時,您也可以為 Servlet 應用程式提供等效的配置。

結論

Spring Security 6.4 是一個令人興奮的版本,為使用 OAuth2 保護的應用程式帶來了許多改進,還包含許多其他令人興奮的功能。在這篇博文中,我們探討了即將釋出版本中的三個新特性。首先,我們討論瞭如何在非響應式應用程式中使用 RestClient 發起受保護資源請求而無需額外依賴。接下來,我們介紹了選擇在任何地方使用 RestClient,並享受與響應式技術棧對齊的更精簡、更一致的配置。最後,我們學習瞭如何在 OAuth 2.0 訪問令牌請求中覆蓋或省略預設引數,這解鎖了之前難以解決的高階場景。

我希望您和我一樣對這一輪新的改進以及 Spring Security 6.4 中的所有其他功能感到興奮。這些功能及更多內容已在 Spring Security 6.4.0-RC1 中提供預釋出版本,請嘗試使用。我們很樂意聽取您的反饋!

獲取 Spring 電子報

訂閱 Spring 電子報,保持聯絡

訂閱

快人一步

VMware 提供培訓和認證,助您飛速進步。

瞭解更多

獲取支援

Tanzu Spring 透過一個簡單的訂閱即可獲得 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

近期活動

檢視 Spring 社群的所有近期活動。

檢視全部