OAuth2
Spring Security 提供全面的 OAuth 2.0 支援。本節討論如何將 OAuth 2.0 整合到您的 servlet 基礎應用程式中。
總覽
Spring Security 的 OAuth 2.0 支援包含兩個主要功能集
OAuth2 登入 是一個非常強大的 OAuth2 用戶端功能,值得在參考文件中單獨介紹。但是,它並非作為獨立功能存在,而是需要 OAuth2 用戶端才能運作。 |
這些功能集涵蓋 OAuth 2.0 授權框架 中定義的資源伺服器和用戶端角色,而授權伺服器角色則由 Spring Authorization Server 涵蓋,這是一個基於 Spring Security 建構的獨立專案。
OAuth2 中的資源伺服器和用戶端角色通常由一個或多個伺服器端應用程式表示。此外,授權伺服器角色可以由一個或多個協力廠商表示(例如在組織內集中身分管理和/或驗證的情況下)-或-它可以由一個應用程式表示(例如 Spring Authorization Server 的情況)。
例如,典型的基於 OAuth2 的微服務架構可能包含一個面向使用者的用戶端應用程式、多個提供 REST API 的後端資源伺服器,以及一個用於管理使用者和驗證問題的協力廠商授權伺服器。常見的情況是,單一應用程式僅代表這些角色之一,並且需要與一個或多個提供其他角色的協力廠商整合。
Spring Security 可以處理這些以及更多情境。以下章節將介紹 Spring Security 提供的角色,並包含常見情境的範例。
OAuth2 資源伺服器
本節包含 OAuth2 資源伺服器功能的摘要和範例。如需完整的參考文件,請參閱 OAuth 2.0 資源伺服器。 |
若要開始使用,請將 spring-security-oauth2-resource-server
相依性新增至您的專案。使用 Spring Boot 時,請新增以下 starter
-
Gradle
-
Maven
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
若未使用 Spring Boot,如需其他選項,請參閱 取得 Spring Security。 |
請考慮以下 OAuth2 資源伺服器的使用案例
-
我想使用 OAuth2 保護對 API 的存取(授權伺服器提供 JWT 或不透明存取權杖)
-
我想使用 JWT 保護對 API 的存取(自訂權杖)
使用 OAuth2 存取權杖保護存取
使用 OAuth2 存取權杖保護對 API 的存取非常常見。在大多數情況下,Spring Security 僅需最少的組態即可使用 OAuth2 保護應用程式的安全。
Spring Security 支援兩種 Bearer
權杖類型,每種類型都使用不同的元件進行驗證
JWT 支援
以下範例使用 Spring Boot 組態屬性組態 JwtDecoder
bean
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://my-auth-server.com
使用 Spring Boot 時,這就是所有需要的組態。Spring Boot 提供的預設配置相當於以下內容
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation("https://my-auth-server.com");
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
return http.build()
}
@Bean
fun jwtDecoder(): JwtDecoder {
return JwtDecoders.fromIssuerLocation("https://my-auth-server.com")
}
}
不透明權杖支援
以下範例使用 Spring Boot 組態屬性組態 OpaqueTokenIntrospector
bean
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: https://my-auth-server.com/oauth2/introspect
client-id: my-client-id
client-secret: my-client-secret
使用 Spring Boot 時,這就是所有需要的組態。Spring Boot 提供的預設配置相當於以下內容
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.opaqueToken(Customizer.withDefaults())
);
return http.build();
}
@Bean
public OpaqueTokenIntrospector opaqueTokenIntrospector() {
return new SpringOpaqueTokenIntrospector(
"https://my-auth-server.com/oauth2/introspect", "my-client-id", "my-client-secret");
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken { }
}
}
return http.build()
}
@Bean
fun opaqueTokenIntrospector(): OpaqueTokenIntrospector {
return SpringOpaqueTokenIntrospector(
"https://my-auth-server.com/oauth2/introspect", "my-client-id", "my-client-secret"
)
}
}
使用自訂 JWT 保護存取
使用 JWT 保護對 API 的存取是一個相當常見的目標,尤其是在前端開發為單頁應用程式時。Spring Security 中的 OAuth2 資源伺服器支援可用於任何類型的 Bearer
權杖,包括自訂 JWT。
使用 JWT 保護 API 所需的只是 JwtDecoder
bean,它用於驗證簽章並解碼權杖。Spring Security 將自動使用提供的 bean 在 SecurityFilterChain
內組態保護。
以下範例使用 Spring Boot 組態屬性組態 JwtDecoder
bean
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:my-public-key.pub
您可以將公鑰作為類別路徑資源提供(在本範例中稱為 |
使用 Spring Boot 時,這就是所有需要的組態。Spring Boot 提供的預設配置相當於以下內容
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(publicKey()).build();
}
private RSAPublicKey publicKey() {
// ...
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
return http.build()
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withPublicKey(publicKey()).build()
}
private fun publicKey(): RSAPublicKey {
// ...
}
}
Spring Security 不提供用於鑄造權杖的端點。但是,Spring Security 確實提供了 |
OAuth2 用戶端
本節包含 OAuth2 用戶端功能的摘要和範例。如需完整的參考文件,請參閱 OAuth 2.0 用戶端 和 OAuth 2.0 登入。 |
若要開始使用,請將 spring-security-oauth2-client
相依性新增至您的專案。使用 Spring Boot 時,請新增以下 starter
-
Gradle
-
Maven
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
若未使用 Spring Boot,如需其他選項,請參閱 取得 Spring Security。 |
請考慮以下 OAuth2 用戶端的使用案例
使用 OAuth2 讓使用者登入
讓使用者透過 OAuth2 登入非常常見。OpenID Connect 1.0 提供一個名為 id_token
的特殊權杖,旨在為 OAuth2 用戶端提供執行使用者身分驗證和讓使用者登入的能力。在某些情況下,OAuth2 可以直接用於讓使用者登入(例如,流行的社交登入提供者,例如 GitHub 和 Facebook,它們未實作 OpenID Connect)。
以下範例組態應用程式作為 OAuth2 用戶端,能夠使用 OAuth2 或 OpenID Connect 讓使用者登入
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.oauth2Login(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
oauth2Login { }
}
return http.build()
}
}
除了上述組態之外,應用程式還需要至少一個 ClientRegistration
透過使用 ClientRegistrationRepository
bean 進行組態。以下範例使用 Spring Boot 組態屬性組態 InMemoryClientRegistrationRepository
bean
spring:
security:
oauth2:
client:
registration:
my-oidc-client:
provider: my-oidc-provider
client-id: my-client-id
client-secret: my-client-secret
authorization-grant-type: authorization_code
scope: openid,profile
provider:
my-oidc-provider:
issuer-uri: https://my-oidc-provider.com
透過上述組態,應用程式現在支援兩個額外的端點
-
登入端點(例如
/oauth2/authorization/my-oidc-client
)用於啟動登入並執行重新導向至協力廠商授權伺服器。 -
重新導向端點(例如
/login/oauth2/code/my-oidc-client
)由授權伺服器用於重新導向回用戶端應用程式,並且將包含一個code
參數,用於透過存取權杖請求取得id_token
和/或access_token
。
上述組態中 |
存取受保護的資源
向受 OAuth2 保護的協力廠商 API 發出請求是 OAuth2 用戶端的核心使用案例。這是透過授權用戶端(由 Spring Security 中的 OAuth2AuthorizedClient
類別表示)並透過將 Bearer
權杖放在輸出請求的 Authorization
標頭中來存取受保護的資源來完成的。
以下範例組態應用程式作為 OAuth2 用戶端,能夠從協力廠商 API 請求受保護的資源
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.oauth2Client(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
oauth2Client { }
}
return http.build()
}
}
上述範例未提供讓使用者登入的方式。您可以使用任何其他登入機制(例如 |
除了上述組態之外,應用程式還需要至少一個 ClientRegistration
透過使用 ClientRegistrationRepository
bean 進行組態。以下範例使用 Spring Boot 組態屬性組態 InMemoryClientRegistrationRepository
bean
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
scope: message.read,message.write
provider:
my-auth-server:
issuer-uri: https://my-auth-server.com
除了組態 Spring Security 以支援 OAuth2 用戶端功能之外,您還需要決定將如何存取受保護的資源,並據此組態您的應用程式。Spring Security 提供 OAuth2AuthorizedClientManager
的實作,用於取得可用於存取受保護資源的存取權杖。
當預設 |
使用 OAuth2AuthorizedClientManager
最簡單的方式是透過 ExchangeFilterFunction
,它會攔截透過 WebClient
的請求。若要使用 WebClient
,您需要新增 spring-webflux
相依性以及反應式用戶端實作
-
Gradle
-
Maven
implementation 'org.springframework:spring-webflux'
implementation 'io.projectreactor.netty:reactor-netty'
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
以下範例使用預設 OAuth2AuthorizedClientManager
組態 WebClient
,使其能夠透過將 Bearer
權杖放在每個請求的 Authorization
標頭中來存取受保護的資源
ExchangeFilterFunction
組態 WebClient
-
Java
-
Kotlin
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction filter =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(filter.oauth2Configuration())
.build();
}
}
@Configuration
class WebClientConfig {
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager): WebClient {
val filter = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
return WebClient.builder()
.apply(filter.oauth2Configuration())
.build()
}
}
此組態的 WebClient
可以如以下範例中使用
WebClient
存取受保護的資源-
Java
-
Kotlin
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;
@RestController
public class MessagesController {
private final WebClient webClient;
public MessagesController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/messages")
public ResponseEntity<List<Message>> messages() {
return this.webClient.get()
.uri("https://127.0.0.1:8090/messages")
.attributes(clientRegistrationId("my-oauth2-client"))
.retrieve()
.toEntityList(Message.class)
.block();
}
public record Message(String message) {
}
}
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId
@RestController
class MessagesController(private val webClient: WebClient) {
@GetMapping("/messages")
fun messages(): ResponseEntity<List<Message>> {
return webClient.get()
.uri("https://127.0.0.1:8090/messages")
.attributes(clientRegistrationId("my-oauth2-client"))
.retrieve()
.toEntityList<Message>()
.block()!!
}
data class Message(val message: String)
}
為目前使用者存取受保護的資源
當使用者透過 OAuth2 或 OpenID Connect 登入時,授權伺服器可能會提供可用於直接存取受保護資源的存取權杖。這很方便,因為它只需要為這兩個使用案例同時組態單一 ClientRegistration
。
本節將 使用 OAuth2 讓使用者登入 和 存取受保護的資源 合併到單一組態中。還存在其他進階情境,例如組態一個用於登入的 |
以下範例組態應用程式作為 OAuth2 用戶端,能夠讓使用者登入並從協力廠商 API 請求受保護的資源
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.oauth2Login(Customizer.withDefaults())
.oauth2Client(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
oauth2Login { }
oauth2Client { }
}
return http.build()
}
}
除了上述組態之外,應用程式還需要至少一個 ClientRegistration
透過使用 ClientRegistrationRepository
bean 進行組態。以下範例使用 Spring Boot 組態屬性組態 InMemoryClientRegistrationRepository
bean
spring:
security:
oauth2:
client:
registration:
my-combined-client:
provider: my-auth-server
client-id: my-client-id
client-secret: my-client-secret
authorization-grant-type: authorization_code
scope: openid,profile,message.read,message.write
provider:
my-auth-server:
issuer-uri: https://my-auth-server.com
先前範例(使用 OAuth2 讓使用者登入、存取受保護的資源)與此範例之間的主要差異是透過 |
除了組態 Spring Security 以支援 OAuth2 用戶端功能之外,您還需要決定將如何存取受保護的資源,並據此組態您的應用程式。Spring Security 提供 OAuth2AuthorizedClientManager
的實作,用於取得可用於存取受保護資源的存取權杖。
當預設 |
使用 OAuth2AuthorizedClientManager
最簡單的方式是透過 ExchangeFilterFunction
,它會攔截透過 WebClient
的請求。若要使用 WebClient
,您需要新增 spring-webflux
相依性以及反應式用戶端實作
-
Gradle
-
Maven
implementation 'org.springframework:spring-webflux'
implementation 'io.projectreactor.netty:reactor-netty'
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
以下範例使用預設 OAuth2AuthorizedClientManager
組態 WebClient
,使其能夠透過將 Bearer
權杖放在每個請求的 Authorization
標頭中來存取受保護的資源
ExchangeFilterFunction
組態 WebClient
-
Java
-
Kotlin
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction filter =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(filter.oauth2Configuration())
.build();
}
}
@Configuration
class WebClientConfig {
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager): WebClient {
val filter = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
return WebClient.builder()
.apply(filter.oauth2Configuration())
.build()
}
}
此組態的 WebClient
可以如以下範例中使用
WebClient
存取受保護的資源(目前使用者)-
Java
-
Kotlin
@RestController
public class MessagesController {
private final WebClient webClient;
public MessagesController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/messages")
public ResponseEntity<List<Message>> messages() {
return this.webClient.get()
.uri("https://127.0.0.1:8090/messages")
.retrieve()
.toEntityList(Message.class)
.block();
}
public record Message(String message) {
}
}
@RestController
class MessagesController(private val webClient: WebClient) {
@GetMapping("/messages")
fun messages(): ResponseEntity<List<Message>> {
return webClient.get()
.uri("https://127.0.0.1:8090/messages")
.retrieve()
.toEntityList<Message>()
.block()!!
}
data class Message(val message: String)
}
與先前的範例不同,請注意,我們不需要告知 Spring Security 我們想要使用的 |
啟用擴充授權類型
常見的使用案例包括啟用和/或組態擴充授權類型。例如,Spring Security 提供對 jwt-bearer
和 token-exchange
授權類型的支援,但預設情況下不會啟用它們,因為它們不是核心 OAuth 2.0 規範的一部分。
在 Spring Security 6.2 和更新版本中,我們可以簡單地發佈一個或多個 OAuth2AuthorizedClientProvider
的 bean,它們將會自動被選取。以下範例僅啟用 jwt-bearer
授權類型
jwt-bearer
授權類型-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public OAuth2AuthorizedClientProvider jwtBearer() {
return new JwtBearerOAuth2AuthorizedClientProvider();
}
}
@Configuration
class SecurityConfig {
@Bean
fun jwtBearer(): OAuth2AuthorizedClientProvider {
return JwtBearerOAuth2AuthorizedClientProvider()
}
}
當預設 OAuth2AuthorizedClientManager
尚未提供時,Spring Security 將會自動發佈一個。
任何自訂 |
為了在 Spring Security 6.2 之前實現上述組態,我們必須自行發佈此 bean,並確保我們也重新啟用了預設授權類型。若要瞭解幕後組態的內容,以下是組態可能看起來的樣子
jwt-bearer
授權類型(6.2 之前)-
Java
-
Kotlin
@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;
}
}
@Configuration
class SecurityConfig {
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientRepository: OAuth2AuthorizedClientRepository
): OAuth2AuthorizedClientManager {
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.provider(JwtBearerOAuth2AuthorizedClientProvider())
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository
)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
}
自訂現有的授權類型
透過發佈 bean 來啟用擴充授權類型的能力也提供了自訂現有授權類型的機會,而無需重新定義預設值。例如,如果我們想要自訂 client_credentials
授權的 OAuth2AuthorizedClientProvider
的時鐘偏移,我們可以簡單地發佈如下所示的 bean
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public OAuth2AuthorizedClientProvider clientCredentials() {
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider =
new ClientCredentialsOAuth2AuthorizedClientProvider();
authorizedClientProvider.setClockSkew(Duration.ofMinutes(5));
return authorizedClientProvider;
}
}
@Configuration
class SecurityConfig {
@Bean
fun clientCredentials(): OAuth2AuthorizedClientProvider {
val authorizedClientProvider = ClientCredentialsOAuth2AuthorizedClientProvider()
authorizedClientProvider.setClockSkew(Duration.ofMinutes(5))
return authorizedClientProvider
}
}
自訂權杖請求參數
取得存取權杖時,自訂請求參數的需求相當常見。例如,假設我們想要將自訂 audience
參數新增至權杖請求,因為提供者針對 authorization_code
授權需要此參數。
在 Spring Security 6.2 和更新版本中,我們可以簡單地發佈類型為 OAuth2AccessTokenResponseClient
且泛型類型為 OAuth2AuthorizationCodeGrantRequest
的 bean,Spring Security 將會使用它來組態 OAuth2 用戶端元件。
以下範例自訂 authorization_code
授權的權杖請求參數,而不使用 DSL
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> 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() {
return (grantRequest) -> {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set("audience", "xyz_value");
return parameters;
};
}
}
@Configuration
class SecurityConfig {
@Bean
fun authorizationCodeAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter()
requestEntityConverter.addParametersConverter(parametersConverter())
val accessTokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter)
return accessTokenResponseClient
}
private fun parametersConverter(): Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> {
return Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> { grantRequest ->
LinkedMultiValueMap<String, String>().also { parameters ->
parameters["audience"] = "xyz_value"
}
}
}
}
請注意,在這種情況下,我們不需要自訂 |
在 Spring Security 6.2 之前,我們必須確保此自訂套用於 OAuth2 登入(如果我們正在使用此功能)和使用 Spring Security DSL 的 OAuth2 用戶端元件。若要瞭解幕後組態的內容,以下是組態可能看起來的樣子
-
Java
-
Kotlin
@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()
)
.oauth2Login((oauth2Login) -> oauth2Login
.tokenEndpoint((tokenEndpoint) -> tokenEndpoint
.accessTokenResponseClient(accessTokenResponseClient)
)
)
.oauth2Client((oauth2Client) -> oauth2Client
.authorizationCodeGrant((authorizationCode) -> authorizationCode
.accessTokenResponseClient(accessTokenResponseClient)
)
);
return http.build();
}
private static Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter() {
// ...
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter()
requestEntityConverter.addParametersConverter(parametersConverter())
val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
tokenResponseClient.setRequestEntityConverter(requestEntityConverter)
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2Login {
tokenEndpoint {
accessTokenResponseClient = tokenResponseClient
}
}
oauth2Client {
authorizationCodeGrant {
accessTokenResponseClient = tokenResponseClient
}
}
}
return http.build()
}
private fun parametersConverter(): Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> {
// ...
}
}
對於其他授權類型,我們可以發佈額外的 OAuth2AccessTokenResponseClient
bean 以覆寫預設值。例如,若要自訂 client_credentials
授權的權杖請求,我們可以發佈以下 bean
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> 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() {
// ...
}
}
@Configuration
class SecurityConfig {
@Bean
fun clientCredentialsAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter()
requestEntityConverter.addParametersConverter(parametersConverter())
val accessTokenResponseClient = DefaultClientCredentialsTokenResponseClient()
accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter)
return accessTokenResponseClient
}
private fun parametersConverter(): Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>> {
// ...
}
}
Spring Security 會自動解析以下 OAuth2AccessTokenResponseClient
bean 的泛型類型
-
OAuth2AuthorizationCodeGrantRequest
(請參閱DefaultAuthorizationCodeTokenResponseClient
) -
OAuth2RefreshTokenGrantRequest
(請參閱DefaultRefreshTokenTokenResponseClient
) -
OAuth2ClientCredentialsGrantRequest
(請參閱DefaultClientCredentialsTokenResponseClient
) -
OAuth2PasswordGrantRequest
(請參閱DefaultPasswordTokenResponseClient
) -
JwtBearerGrantRequest
(請參閱DefaultJwtBearerTokenResponseClient
) -
TokenExchangeGrantRequest
(請參閱DefaultTokenExchangeTokenResponseClient
)
發佈類型為 |
發佈類型為 |
自訂 OAuth2 用戶端元件使用的 RestOperations
另一個常見的使用案例是需要自訂取得存取權杖時使用的 RestOperations
。我們可能需要這樣做來自訂回應的處理(透過自訂 HttpMessageConverter
)或針對企業網路套用 Proxy 設定(透過自訂的 ClientHttpRequestFactory
)。
在 Spring Security 6.2 和更新版本中,我們可以簡單地發佈類型為 OAuth2AccessTokenResponseClient
的 bean,Spring Security 將會為我們組態並發佈 OAuth2AuthorizedClientManager
bean。
以下範例自訂所有支援的授權類型的 RestOperations
RestOperations
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
DefaultRefreshTokenTokenResponseClient accessTokenResponseClient =
new DefaultRefreshTokenTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
DefaultPasswordTokenResponseClient accessTokenResponseClient =
new DefaultPasswordTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
DefaultJwtBearerTokenResponseClient accessTokenResponseClient =
new DefaultJwtBearerTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
DefaultTokenExchangeTokenResponseClient accessTokenResponseClient =
new DefaultTokenExchangeTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
return accessTokenResponseClient;
}
@Bean
public RestTemplate restTemplate() {
// ...
}
}
@Configuration
class SecurityConfig {
@Bean
fun authorizationCodeAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
val accessTokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
accessTokenResponseClient.setRestOperations(restTemplate())
return accessTokenResponseClient
}
@Bean
fun refreshTokenAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
val accessTokenResponseClient = DefaultRefreshTokenTokenResponseClient()
accessTokenResponseClient.setRestOperations(restTemplate())
return accessTokenResponseClient
}
@Bean
fun clientCredentialsAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
val accessTokenResponseClient = DefaultClientCredentialsTokenResponseClient()
accessTokenResponseClient.setRestOperations(restTemplate())
return accessTokenResponseClient
}
@Bean
fun passwordAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
val accessTokenResponseClient = DefaultPasswordTokenResponseClient()
accessTokenResponseClient.setRestOperations(restTemplate())
return accessTokenResponseClient
}
@Bean
fun jwtBearerAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
val accessTokenResponseClient = DefaultJwtBearerTokenResponseClient()
accessTokenResponseClient.setRestOperations(restTemplate())
return accessTokenResponseClient
}
@Bean
fun tokenExchangeAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> {
val accessTokenResponseClient = DefaultTokenExchangeTokenResponseClient()
accessTokenResponseClient.setRestOperations(restTemplate())
return accessTokenResponseClient
}
@Bean
fun restTemplate(): RestTemplate {
// ...
}
}
當預設 OAuth2AuthorizedClientManager
尚未提供時,Spring Security 將會自動發佈一個。
請注意,在這種情況下,我們不需要自訂 |
在 Spring Security 6.2 之前,我們必須確保此自訂套用於 OAuth2 登入(如果我們正在使用此功能)和 OAuth2 用戶端元件。我們必須同時使用 Spring Security DSL(針對 authorization_code
授權)並發佈類型為 OAuth2AuthorizedClientManager
的 bean,用於其他授權類型。若要瞭解幕後組態的內容,以下是組態可能看起來的樣子
RestOperations
(6.2 之前)-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRestOperations(restTemplate());
http
// ...
.oauth2Login((oauth2Login) -> oauth2Login
.tokenEndpoint((tokenEndpoint) -> tokenEndpoint
.accessTokenResponseClient(accessTokenResponseClient)
)
)
.oauth2Client((oauth2Client) -> oauth2Client
.authorizationCodeGrant((authorizationCode) -> authorizationCode
.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());
DefaultJwtBearerTokenResponseClient jwtBearerAccessTokenResponseClient =
new DefaultJwtBearerTokenResponseClient();
jwtBearerAccessTokenResponseClient.setRestOperations(restTemplate());
JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider =
new JwtBearerOAuth2AuthorizedClientProvider();
jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient);
DefaultTokenExchangeTokenResponseClient tokenExchangeAccessTokenResponseClient =
new DefaultTokenExchangeTokenResponseClient();
tokenExchangeAccessTokenResponseClient.setRestOperations(restTemplate());
TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider =
new TokenExchangeOAuth2AuthorizedClientProvider();
tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient);
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken((refreshToken) -> refreshToken
.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
)
.clientCredentials((clientCredentials) -> clientCredentials
.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
)
.password((password) -> password
.accessTokenResponseClient(passwordAccessTokenResponseClient)
)
.provider(jwtBearerAuthorizedClientProvider)
.provider(tokenExchangeAuthorizedClientProvider)
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
public RestTemplate restTemplate() {
// ...
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
tokenResponseClient.setRestOperations(restTemplate())
http {
// ...
oauth2Login {
tokenEndpoint {
accessTokenResponseClient = tokenResponseClient
}
}
oauth2Client {
authorizationCodeGrant {
accessTokenResponseClient = tokenResponseClient
}
}
}
return http.build()
}
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ClientRegistrationRepository?,
authorizedClientRepository: OAuth2AuthorizedClientRepository?
): OAuth2AuthorizedClientManager {
val refreshTokenAccessTokenResponseClient = DefaultRefreshTokenTokenResponseClient()
refreshTokenAccessTokenResponseClient.setRestOperations(restTemplate())
val clientCredentialsAccessTokenResponseClient = DefaultClientCredentialsTokenResponseClient()
clientCredentialsAccessTokenResponseClient.setRestOperations(restTemplate())
val passwordAccessTokenResponseClient = DefaultPasswordTokenResponseClient()
passwordAccessTokenResponseClient.setRestOperations(restTemplate())
val jwtBearerAccessTokenResponseClient = DefaultJwtBearerTokenResponseClient()
jwtBearerAccessTokenResponseClient.setRestOperations(restTemplate())
val jwtBearerAuthorizedClientProvider = JwtBearerOAuth2AuthorizedClientProvider()
jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient)
val tokenExchangeAccessTokenResponseClient = DefaultTokenExchangeTokenResponseClient()
tokenExchangeAccessTokenResponseClient.setRestOperations(restTemplate())
val tokenExchangeAuthorizedClientProvider = TokenExchangeOAuth2AuthorizedClientProvider()
tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient)
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken { refreshToken ->
refreshToken.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
}
.clientCredentials { clientCredentials ->
clientCredentials.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
}
.password { password ->
password.accessTokenResponseClient(passwordAccessTokenResponseClient)
}
.provider(jwtBearerAuthorizedClientProvider)
.provider(tokenExchangeAuthorizedClientProvider)
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository
)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
@Bean
fun restTemplate(): RestTemplate {
// ...
}
}