先行一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多在 Spring Security 5 中,隨著 OAuth2 Resource Server 和 OAuth2 Client 被引入框架,OAuth2 的發展取得了很大進展。
如今,利用 OAuth2 Resource Server 中提供的功能開發由 OAuth2 保護的應用非常方便。此外,我們可以利用 OAuth2 Client 的功能與 OAuth 2.0 和 OpenID Connect 1.0 提供商整合,從而可以透過 OAuth2 登入來認證使用者,和/或向由 OAuth2 保護的應用傳送受保護的請求。
然而,OAuth2 生態系統非常複雜,並且通常需要進行定製以與那些對各種 OAuth2 相關標準實現不靈活甚至不合規的第三方整合。考慮到所有這些複雜性,Spring Security 的 OAuth2 Client 元件在開發時就非常注重靈活性。這種靈活性伴隨著權衡,尤其是在配置方面。
我們聽取了社群關於配置的反饋意見,一個共同的主題是簡化各種 OAuth2 Client 元件的配置。讓我們看看在最新的 Spring Security 里程碑版本 6.2.0-M2 中配置是如何被簡化的。
更新: 參考文件的 OAuth2 頁面已更新,包含了 OAuth2 Client 的概述以及基於本文的示例。
讓我們從 start.spring.io 上的一個簡單應用開始,我們可以以此為基礎構建各種可能遇到的用例。以下配置等同於 Spring Boot 提供的預設安排。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2Client(Customizer.withDefaults())
.oauth2Login(Customizer.withDefaults());
return http.build();
}
}
所需的一切僅僅是在 application.yml
中配置一個 ClientRegistration
,如下所示:
spring:
security:
oauth2:
client:
registration:
my-oauth2-client:
provider: my-auth-server
client-id: my-client-id
client-secret: my-client-secret
authorization-grant-type: authorization_code
client-authentication-method: client_secret_basic
scope: openid,profile,message.read,message.write
provider:
my-auth-server:
issuer-uri: https://my-auth-server.com
考慮到上述配置,讓我們思考以下用例:
一個常見的用例是,在獲取 access_token
時需要定製請求引數。例如,假設我們想在令牌請求中新增一個自定義的 audience
引數,因為提供商要求 authorization_code
授權型別必須有此引數。
以前,我們必須使用 Spring Security DSL 確保此定製既適用於 OAuth2 登入(如果我們使用此功能),也適用於 OAuth2 Client 元件。配置可能看起來像這樣:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(parametersConverter());
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2Client((oauth2Client) -> oauth2Client
.authorizationCodeGrant((authorizationCode) -> authorizationCode
.accessTokenResponseClient(accessTokenResponseClient)
)
)
.oauth2Login((oauth2Login) -> oauth2Login
.tokenEndpoint((tokenEndpoint) -> tokenEndpoint
.accessTokenResponseClient(accessTokenResponseClient)
)
);
return http.build();
}
private static Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter() {
return (grantRequest) -> {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set("audience", "xyz_value");
return parameters;
};
}
}
在最新的里程碑版本中,我們可以簡單地釋出一個型別為 OAuth2AccessTokenResponseClient<T>
的 bean(其中 T
是 OAuth2AuthorizationCodeGrantRequest
),它就會被自動檢測到。現在可以將此配置簡化為:
@Configuration
public class SecurityConfig {
@Bean
public DefaultAuthorizationCodeTokenResponseClient authorizationCodeAccessTokenResponseClient() {
OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(parametersConverter());
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
return accessTokenResponseClient;
}
private static Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter() {
// ...
}
}
注意: 請注意,由於這是我們進行的唯一定製,我們實際上可以完全省略 SecurityFilterChain
bean,並使用 Spring Boot 提供的預設配置。如果我們需要配置其他內容,情況可能並非總是如此,但這仍然值得考慮,因為無論如何我們的配置都更簡單了。
對於其他授權型別,我們也可以釋出類似的 bean。例如,要定製 client_credentials
授權型別的令牌請求,我們可以釋出以下 bean:
@Configuration
public class SecurityConfig {
@Bean
public DefaultClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient() {
OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
new OAuth2ClientCredentialsGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(parametersConverter());
DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
return accessTokenResponseClient;
}
private static Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>> parametersConverter() {
// ...
}
}
RestOperations
另一個常見的用例是,在獲取 access_token
時需要定製使用的 RestOperations
(或響應式應用中的 WebClient
)。我們可能需要這樣做來定製響應處理(透過自定義 HttpMessageConverter
),或為企業網路應用代理設定(透過定製的 ClientHttpRequestFactory
)。
假設我們想同時定製多種授權型別。以前,我們必須確保此定製既適用於 OAuth2 登入(如果我們使用此功能),也適用於 OAuth2 Client 元件。我們既要使用 Spring Security DSL(針對 authorization_code
授權型別),又要為其他授權型別釋出一個型別為 OAuth2AuthorizedClientManager
的 bean,這需要非常冗長的配置。配置可能看起來像這樣:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2Client((oauth2Client) -> oauth2Client
.authorizationCodeGrant((authorizationCode) -> authorizationCode
.accessTokenResponseClient(accessTokenResponseClient)
)
)
.oauth2Login((oauth2Login) -> oauth2Login
.tokenEndpoint((tokenEndpoint) -> tokenEndpoint
.accessTokenResponseClient(accessTokenResponseClient)
)
);
return http.build();
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
DefaultRefreshTokenTokenResponseClient refreshTokenAccessTokenResponseClient =
new DefaultRefreshTokenTokenResponseClient();
refreshTokenAccessTokenResponseClient.setRestOperations(restTemplate());
DefaultClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
clientCredentialsAccessTokenResponseClient.setRestOperations(restTemplate());
DefaultPasswordTokenResponseClient passwordAccessTokenResponseClient =
new DefaultPasswordTokenResponseClient();
passwordAccessTokenResponseClient.setRestOperations(restTemplate());
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken((refreshToken) -> refreshToken
.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
)
.clientCredentials((clientCredentials) -> clientCredentials
.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
)
.password((password) -> password
.accessTokenResponseClient(passwordAccessTokenResponseClient)
)
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
public RestTemplate restTemplate() {
// ...
}
}
在最新的里程碑版本中,我們可以簡單地為每種 OAuth2AccessTokenResponseClient<T>
型別(其中 T
是 Spring Security 開箱即用支援的授權型別)釋出 bean。現在可以將此配置簡化為:
@Configuration
public class SecurityConfig {
@Bean
public DefaultAuthorizationCodeTokenResponseClient authorizationCodeAccessTokenResponseClient() {
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public DefaultRefreshTokenTokenResponseClient refreshTokenAccessTokenResponseClient() {
DefaultRefreshTokenTokenResponseClient accessTokenResponseClient =
new DefaultRefreshTokenTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public DefaultClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient() {
DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public DefaultPasswordTokenResponseClient passwordAccessTokenResponseClient() {
DefaultPasswordTokenResponseClient accessTokenResponseClient =
new DefaultPasswordTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public RestTemplate restTemplate() {
// ...
}
}
實際上,我們甚至可以透過釋出相應的 OAuth2AccessTokenResponseClient
bean 來選擇啟用擴充套件授權型別 jwt-bearer
。
@Bean
public DefaultJwtBearerTokenResponseClient jwtBearerAccessTokenResponseClient() {
DefaultJwtBearerTokenResponseClient accessTokenResponseClient =
new DefaultJwtBearerTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
注意: 請注意,我們不需要釋出型別為 OAuth2AuthorizedClientManager
的 bean。現在 Spring Security 會為我們釋出一個。
現在我們可以透過依賴注入使用完全配置好的 OAuth2AuthorizedClientManager
,例如這樣:
@RestController
class MyController {
private final OAuth2AuthorizedClientManager authorizedClientManager;
MyController(OAuth2AuthorizedClientManager authorizedClientManager) {
this.authorizedClientManager = authorizedClientManager;
}
// ...
}
另一個用例涉及啟用和/或配置擴充套件授權型別。例如,Spring Security 支援 jwt-bearer
授權型別,但預設不啟用它。
以前,我們必須釋出一個型別為 OAuth2AuthorizedClientManager
的 bean,並確保同時重新啟用預設授權型別,這需要一些冗長的配置。配置可能看起來像這樣:
@Configuration
public class SecurityConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.provider(new JwtBearerOAuth2AuthorizedClientProvider())
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
在最新的里程碑版本中,我們可以簡單地釋出一個或多個 OAuth2AuthorizedClientProvider
的 bean,它們就會被自動檢測到。現在可以將此配置簡化為:
@Configuration
public class SecurityConfig {
@Bean
public OAuth2AuthorizedClientProvider jwtBearer() {
return new JwtBearerOAuth2AuthorizedClientProvider();
}
}
注意: 任何釋出的、非 Spring Security 提供的型別為 OAuth2AuthorizedClientProvider
的 bean 也將被檢測到,並在預設授權型別之後應用。
這還提供了定製現有授權型別、而無需重新定義預設配置的機會。例如,如果想定製 client_credentials
授權型別對應的 OAuth2AuthorizedClientProvider
的時鐘偏差(clock skew),我們可以簡單地釋出一個 bean,例如這樣:
@Configuration
public class SecurityConfig {
@Bean
public OAuth2AuthorizedClientProvider clientCredentials() {
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider =
new ClientCredentialsOAuth2AuthorizedClientProvider();
authorizedClientProvider.setClockSkew(Duration.ofMinutes(5));
return authorizedClientProvider;
}
}
我希望您和我一樣對 Spring Security 中只需透過釋出 @Bean
即可簡化 OAuth2 Client 元件配置的方法感到興奮。如果您想參與其中,請嘗試使用這個里程碑版本並給我們反饋!我們將繼續傾聽並尋找機會,為 Spring Security 的使用者簡化配置。