此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.5spring-doc.cadn.net.cn

授权客户端功能

本节介绍了 Spring Security for OAuth2 客户端提供的其他功能。spring-doc.cadn.net.cn

解析授权客户端

@RegisteredOAuth2AuthorizedClientannotation 提供了将 method 参数解析为 type 为 argument 值的功能OAuth2AuthorizedClient. 与访问OAuth2AuthorizedClient通过使用OAuth2AuthorizedClientManagerOAuth2AuthorizedClientService. 以下示例演示如何使用@RegisteredOAuth2AuthorizedClient:spring-doc.cadn.net.cn

@Controller
public class OAuth2ClientController {

	@GetMapping("/")
	public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
		OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

		...

		return "index";
	}
}
@Controller
class OAuth2ClientController {
    @GetMapping("/")
    fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String {
        val accessToken = authorizedClient.accessToken

        ...

        return "index"
    }
}

@RegisteredOAuth2AuthorizedClientannotation 由OAuth2AuthorizedClientArgumentResolver,它直接使用OAuth2AuthorizedClientManager因此,继承了它的能力。spring-doc.cadn.net.cn

RestClient 集成

支持RestClientOAuth2ClientHttpRequestInterceptor. 此侦听器提供了通过放置Bearertoken 中Authorization标头。 拦截器直接使用OAuth2AuthorizedClientManager因此继承了以下能力:spring-doc.cadn.net.cn

以下示例使用默认的OAuth2AuthorizedClientManager要配置RestClient能够通过以下方式访问受保护的资源Bearer令牌Authorization标头:spring-doc.cadn.net.cn

配置RestClientClientHttpRequestInterceptor
@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);

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

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)

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

}

提供clientRegistrationId

OAuth2ClientHttpRequestInterceptor使用ClientRegistrationIdResolver来确定使用哪个客户端来获取 Access Token。 默认情况下,RequestAttributeClientRegistrationIdResolver用于解析clientRegistrationIdHttpRequest#attributes().spring-doc.cadn.net.cn

以下示例演示如何提供clientRegistrationIdvia 属性:spring-doc.cadn.net.cn

提供clientRegistrationIdvia 属性
import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;

@Controller
public class ResourceController {

	private final RestClient restClient;

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

	@GetMapping("/")
	public String index() {
		String resourceUri = "...";

		String body = this.restClient.get()
				.uri(resourceUri)
				.attributes(clientRegistrationId("okta"))   (1)
				.retrieve()
				.body(String.class);

		// ...

		return "index";
	}

}
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId
import org.springframework.web.client.body

@Controller
class ResourceController(private restClient: RestClient) {

	@GetMapping("/")
	fun index(): String {
		val resourceUri = "..."

		val body: String = restClient.get()
				.uri(resourceUri)
				.attributes(clientRegistrationId("okta"))   (1)
				.retrieve()
				.body<String>()

		// ...

		return "index"
	}

}
1 clientRegistrationId() is a static method in RequestAttributeClientRegistrationIdResolver.

Alternatively, a custom ClientRegistrationIdResolver can be provided. The following example configures a custom implementation that resolves the clientRegistrationId from the current user.spring-doc.cadn.net.cn

Configure ClientHttpRequestInterceptor with custom ClientRegistrationIdResolver
@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
		requestInterceptor.setClientRegistrationIdResolver(clientRegistrationIdResolver());

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

	private static ClientRegistrationIdResolver clientRegistrationIdResolver() {
		return (request) -> {
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			return (authentication instanceof OAuth2AuthenticationToken principal)
					? principal.getAuthorizedClientRegistrationId() : null;
		};
	}

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)
		requestInterceptor.setClientRegistrationIdResolver(clientRegistrationIdResolver())

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

	fun clientRegistrationIdResolver(): ClientRegistrationIdResolver {
		return ClientRegistrationIdResolver { request ->
			val authentication = SecurityContextHolder.getContext().getAuthentication()
			return if (authentication instanceof OAuth2AuthenticationToken) {
				authentication.getAuthorizedClientRegistrationId()
			} else {
                null
			}
		}
	}

}

Providing the principal

OAuth2ClientHttpRequestInterceptor uses a PrincipalResolver to determine which principal name is associated with the access token, which allows an application to choose how to scope the OAuth2AuthorizedClient that is stored. By default, SecurityContextHolderPrincipalResolver is used to resolve the current principal from the SecurityContextHolder.spring-doc.cadn.net.cn

Alternatively, the principal can be resolved from HttpRequest#attributes() by configuring RequestAttributePrincipalResolver, as the following example shows:spring-doc.cadn.net.cn

Configure ClientHttpRequestInterceptor with RequestAttributePrincipalResolver
@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
		requestInterceptor.setPrincipalResolver(new RequestAttributePrincipalResolver());

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

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)
		requestInterceptor.setPrincipalResolver(RequestAttributePrincipalResolver())

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

}

The following example demonstrates providing a principal name via attributes that scopes the OAuth2AuthorizedClient to the application instead of the current user:spring-doc.cadn.net.cn

Provide principal name via attributes
import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;
import static org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal;

@Controller
public class ResourceController {

	private final RestClient restClient;

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

	@GetMapping("/")
	public String index() {
		String resourceUri = "...";

		String body = this.restClient.get()
				.uri(resourceUri)
				.attributes(clientRegistrationId("okta"))
				.attributes(principal("my-application"))   (1)
				.retrieve()
				.body(String.class);

		// ...

		return "index";
	}

}
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId
import org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal
import org.springframework.web.client.body

@Controller
class ResourceController(private restClient: RestClient) {

    @GetMapping("/")
	fun index(): String {
		val resourceUri = "..."

		val body: String = restClient.get()
				.uri(resourceUri)
				.attributes(clientRegistrationId("okta"))
				.attributes(principal("my-application"))   (1)
				.retrieve()
				.body<String>()

		// ...

		return "index"
	}

}
1 principal() is a static method in RequestAttributePrincipalResolver.

Handling Failure

If an access token is invalid for any reason (e.g. expired token), it can be beneficial to handle the failure by removing the access token so that it cannot be used again. You can set up the interceptor to do this automatically by providing an OAuth2AuthorizationFailureHandler to remove the access token.spring-doc.cadn.net.cn

The following example uses an OAuth2AuthorizedClientRepository to set up an OAuth2AuthorizationFailureHandler that removes an invalid OAuth2AuthorizedClient within the context of an HttpServletRequest:spring-doc.cadn.net.cn

Configure OAuth2AuthorizationFailureHandler using OAuth2AuthorizedClientRepository
@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager,
			OAuth2AuthorizedClientRepository authorizedClientRepository) {

		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);

		OAuth2AuthorizationFailureHandler authorizationFailureHandler =
			OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientRepository);
		requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler);

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

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager,
			authorizedClientRepository: OAuth2AuthorizedClientRepository): RestClient {

		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)

		val authorizationFailureHandler = OAuth2ClientHttpRequestInterceptor
			.authorizationFailureHandler(authorizedClientRepository)
		requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler)

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

}

Alternatively, an OAuth2AuthorizedClientService can be used to remove an invalid OAuth2AuthorizedClient outside the context of an HttpServletRequest, as the following example shows:spring-doc.cadn.net.cn

Configure OAuth2AuthorizationFailureHandler using OAuth2AuthorizedClientService
@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager,
			OAuth2AuthorizedClientService authorizedClientService) {

		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);

		OAuth2AuthorizationFailureHandler authorizationFailureHandler =
			OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientService);
		requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler);

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

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager,
			authorizedClientService: OAuth2AuthorizedClientService): RestClient {

		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)

		val authorizationFailureHandler = OAuth2ClientHttpRequestInterceptor
			.authorizationFailureHandler(authorizedClientService)
		requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler)

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

}

WebClient Integration for Servlet Environments

The OAuth 2.0 Client support integrates with WebClient by using an ExchangeFilterFunction.spring-doc.cadn.net.cn

The ServletOAuth2AuthorizedClientExchangeFilterFunction provides a mechanism for requesting protected resources by using an OAuth2AuthorizedClient and including the associated OAuth2AccessToken as a Bearer Token. It directly uses an OAuth2AuthorizedClientManager and, therefore, inherits the following capabilities:spring-doc.cadn.net.cn

  • An OAuth2AccessToken is requested if the client has not yet been authorized.spring-doc.cadn.net.cn

  • If the OAuth2AccessToken is expired, it is refreshed (or renewed) if an OAuth2AuthorizedClientProvider is available to perform the authorizationspring-doc.cadn.net.cn

The following code shows an example of how to configure WebClient with OAuth 2.0 Client support:spring-doc.cadn.net.cn

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
	ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
			new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
	return WebClient.builder()
			.apply(oauth2Client.oauth2Configuration())
			.build();
}
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient {
    val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build()
}

Providing the Authorized Client

The ServletOAuth2AuthorizedClientExchangeFilterFunction determines the client to use (for a request) by resolving the OAuth2AuthorizedClient from the ClientRequest.attributes() (request attributes).spring-doc.cadn.net.cn

The following code shows how to set an OAuth2AuthorizedClient as a request attribute:spring-doc.cadn.net.cn

@GetMapping("/")
public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
	String resourceUri = ...

	String body = webClient
			.get()
			.uri(resourceUri)
			.attributes(oauth2AuthorizedClient(authorizedClient))   (1)
			.retrieve()
			.bodyToMono(String.class)
			.block();

	...

	return "index";
}
@GetMapping("/")
fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String {
    val resourceUri: String = ...
    val body: String = webClient
            .get()
            .uri(resourceUri)
            .attributes(oauth2AuthorizedClient(authorizedClient)) (1)
            .retrieve()
            .bodyToMono()
            .block()

    ...

    return "index"
}
1 oauth2AuthorizedClient() is a static method in ServletOAuth2AuthorizedClientExchangeFilterFunction.

The following code shows how to set the ClientRegistration.getRegistrationId() as a request attribute:spring-doc.cadn.net.cn

@GetMapping("/")
public String index() {
	String resourceUri = ...

	String body = webClient
			.get()
			.uri(resourceUri)
			.attributes(clientRegistrationId("okta"))   (1)
			.retrieve()
			.bodyToMono(String.class)
			.block();

	...

	return "index";
}
@GetMapping("/")
fun index(): String {
    val resourceUri: String = ...

    val body: String = webClient
            .get()
            .uri(resourceUri)
            .attributes(clientRegistrationId("okta"))  (1)
            .retrieve()
            .bodyToMono()
            .block()

    ...

    return "index"
}
1 clientRegistrationId() is a static method in ServletOAuth2AuthorizedClientExchangeFilterFunction.

The following code shows how to set an Authentication as a request attribute:spring-doc.cadn.net.cn

@GetMapping("/")
public String index() {
	String resourceUri = ...

	Authentication anonymousAuthentication = new AnonymousAuthenticationToken(
			"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
	String body = webClient
			.get()
			.uri(resourceUri)
			.attributes(authentication(anonymousAuthentication))   (1)
			.retrieve()
			.bodyToMono(String.class)
			.block();

	...

	return "index";
}
@GetMapping("/")
fun index(): String {
    val resourceUri: String = ...

    val anonymousAuthentication: Authentication = AnonymousAuthenticationToken(
            "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"))
    val body: String = webClient
            .get()
            .uri(resourceUri)
            .attributes(authentication(anonymousAuthentication))  (1)
            .retrieve()
            .bodyToMono()
            .block()

    ...

    return "index"
}
1 authentication() is a static method in ServletOAuth2AuthorizedClientExchangeFilterFunction.

It is recommended to be cautious with this feature since all HTTP requests will receive an access token bound to the provided principal.spring-doc.cadn.net.cn

Defaulting the Authorized Client

If neither OAuth2AuthorizedClient or ClientRegistration.getRegistrationId() is provided as a request attribute, the ServletOAuth2AuthorizedClientExchangeFilterFunction can determine the default client to use, depending on its configuration.spring-doc.cadn.net.cn

If setDefaultOAuth2AuthorizedClient(true) is configured and the user has authenticated by using HttpSecurity.oauth2Login(), the OAuth2AccessToken associated with the current OAuth2AuthenticationToken is used.spring-doc.cadn.net.cn

The following code shows the specific configuration:spring-doc.cadn.net.cn

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
	ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
			new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
	oauth2Client.setDefaultOAuth2AuthorizedClient(true);
	return WebClient.builder()
			.apply(oauth2Client.oauth2Configuration())
			.build();
}
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient {
    val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
    oauth2Client.setDefaultOAuth2AuthorizedClient(true)
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build()
}

Be cautious with this feature, since all HTTP requests receive the access token.spring-doc.cadn.net.cn

Alternatively, if setDefaultClientRegistrationId("okta") is configured with a valid ClientRegistration, the OAuth2AccessToken associated with the OAuth2AuthorizedClient is used.spring-doc.cadn.net.cn

The following code shows the specific configuration:spring-doc.cadn.net.cn

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
	ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
			new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
	oauth2Client.setDefaultClientRegistrationId("okta");
	return WebClient.builder()
			.apply(oauth2Client.oauth2Configuration())
			.build();
}
@Bean
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient {
    val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
    oauth2Client.setDefaultClientRegistrationId("okta")
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build()
}

Be cautious with this feature, since all HTTP requests receive the access token.spring-doc.cadn.net.cn