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 的改進而延續,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://127.0.0.1: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://127.0.0.1:8090/messages")
			.attributes(clientRegistrationId("messaging-client"))
			.retrieve()
			.body(Message[].class);

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

	public record Message(String message) {
	}

}

上面的範例使用了一個靜態方法,通過屬性將 "messaging-client"registrationId 提供給攔截器。提供的值與先前提供的 yaml 配置中的值相符,Spring Security 就是通過這種方式知道在獲取存取權杖時要使用哪個客戶端 ID、密碼、授權類型、範圍和其他資訊。

當然,這只是一個範例,您不限於僅在端點中返回結果。您可以在應用程式的任何部分執行此操作,例如負責發出受保護的資源請求並將結果返回到您的應用程式的 @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 應用程式中將 RestClient 用作所有 OAuth2 Client 功能的 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 設定的參數值,因此需要使用一些變通方法才能針對特定使用情境自訂應用程式。現在,反應式應用程式(使用 WebClient)和 servlet 應用程式(使用 RestClient)都可以透過 setParametersConverter() 自訂鉤子來覆寫參數。在此情況下,請務必注意,所有授權類型特定參數和預設參數都會先設定。您的自訂 parametersConverter 提供的任何參數都會覆寫現有參數。

除了覆寫參數之外,現在還可以省略可能被授權伺服器拒絕的參數。例如,當 ClientRegistration#clientAuthenticationMethod 設定為 private_key_jwt 時,我們可以透過包含產生的 JWT 的客戶端斷言來提供客戶端身份驗證。某些授權伺服器可能會選擇拒絕同時包含 client_id client_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 社群中所有即將舉辦的活動。

查看全部