Spring Security 6.4 中 RestClient 對 OAuth2 的支援

工程 | 史蒂夫·裡森伯格 | 2024年10月28日 | ...

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

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

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

讓我們詳細探討 RestClient 的新支援以及 OAuth2 客戶端的其他改進。

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 配置屬性配置了一個 InMemoryClientRegistrationRepository bean,其中包含一個 ClientRegistration

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) {
	}

}

上面的示例使用靜態方法透過屬性向攔截器提供 "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 客戶端功能的 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_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 社群所有即將舉行的活動。

檢視所有