在测试中替换 OAuth2 WebClient
replacing an OAuth2 WebClient in a test
我有一个小的 Spring Boot 2.2 批处理写入 OAuth2 REST API。
我已经能够在 https://medium.com/@asce4s/oauth2-with-spring-webclient-761d16f89cdd 之后配置 WebClient
并且它按预期工作。
@Configuration
public class MyRemoteServiceClientOauth2Config {
@Bean("myRemoteService")
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations,
new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
oauth.setDefaultClientRegistrationId("myRemoteService");
return WebClient.builder()
.filter(oauth)
.build();
}
}
但是,现在我想为我的批处理编写一个集成测试,并且我想避免使用 "real" 授权服务器来获取令牌:我不希望我的测试失败,如果外部服务器已关闭。我希望我的测试是 "autonomous"。
我正在调用的远程服务在我的测试期间被 mockserver
假服务所取代。
这种情况下的最佳做法是什么?
- 对我有用的是仅在使用
@Profile("!test")
和 运行 我的 @ActiveProfiles("test")
测试之外启用上述配置。我还在我的测试中导入了一个特定于测试的配置:
@Configuration
@Profile("test")
public class BatchTestConfiguration {
@Bean("myRemoteService")
public WebClient webClientForTest() {
return WebClient.create();
}
}
但我觉得必须在我的生产配置中添加 @Profile("!test")
不是很好..
- 有没有一种 'cleaner' 方法来替换我正在使用的 WebClient bean,用一个无需先尝试获取令牌就可以调用我的假远程服务的方法?我试图在我的 webClientForTest bean 上放置一个
@Primary
,但它不起作用:生产 bean 仍然启用并且我得到一个异常:
No qualifying bean of type 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
这是生产 bean 需要的参数类型
- 作为测试的一部分,我是否需要启动一个伪造的授权服务器并配置 WebClient 以从中获取虚拟令牌?是否有图书馆尽可能开箱即用?
我和你的情况一样,找到了解决办法。首先,为了查看它的实际效果,我创建了一个 repository with a showcase implementation 包含下面解释的所有内容。
is there an 'cleaner' way to replace the WebClient bean I am using, by one that will call my fake remote service without trying to get a token first ?
我不会在您的测试中替换 WebClient
bean,而是用模拟替换 ReactiveOAuth2AuthorizedClientManager
bean。
为此,您必须稍微修改 MyRemoteServiceClientOauth2Config
。而不是使用现在已弃用的方法 UnAuthenticatedServerOAuth2AuthorizedClientRepository
you configure it this way (this is also more in line with the documented configuration on the Servlet-Stack):
@Configuration
public class MyRemoteServiceClientOauth2Config {
@Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2ClientCredentialsFilter =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(reactiveOAuth2AuthorizedClientManager);
oauth2ClientCredentialsFilter.setDefaultClientRegistrationId("myRemoteService");
return WebClient.builder()
.filter(oauth2ClientCredentialsFilter)
.build();
}
@Bean
public ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager(ReactiveClientRegistrationRepository clientRegistrations,
ReactiveOAuth2AuthorizedClientService authorizedClients) {
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, authorizedClients);
authorizedClientManager.setAuthorizedClientProvider(
new ClientCredentialsReactiveOAuth2AuthorizedClientProvider());
return authorizedClientManager;
}
}
然后你可以创建一个 ReactiveOAuth2AuthorizedClientManager
的模拟,它总是 returns 一个 Mono
of an OAuth2AuthorizedClient
像这样:
@TestComponent
@Primary
public class AlwaysAuthorizedOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
@Value("${spring.security.oauth2.client.registration.myRemoteService.client-id}")
String clientId;
@Value("${spring.security.oauth2.client.registration.myRemoteService.client-secret}")
String clientSecret;
@Value("${spring.security.oauth2.client.provider.some-keycloak.token-uri}")
String tokenUri;
/**
* {@inheritDoc}
*
* @return
*/
@Override
public Mono<OAuth2AuthorizedClient> authorize(final OAuth2AuthorizeRequest authorizeRequest) {
return Mono.just(
new OAuth2AuthorizedClient(
ClientRegistration
.withRegistrationId("myRemoteService")
.clientId(clientId)
.clientSecret(clientSecret)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.tokenUri(tokenUri)
.build(),
"some-keycloak",
new OAuth2AccessToken(TokenType.BEARER,
"c29tZS10b2tlbg==",
Instant.now().minus(Duration.ofMinutes(1)),
Instant.now().plus(Duration.ofMinutes(4)))));
}
}
最后 @Import
在你的测试中:
@SpringBootTest
@Import(AlwaysAuthorizedOAuth2AuthorizedClientManager.class)
class YourIntegrationTestClass {
// here is your test code
}
对应的src/test/resources/application.yml
是这样的:
spring:
security:
oauth2:
client:
registration:
myRemoteService:
authorization-grant-type: client_credentials
client-id: test-client
client-secret: 6b30087f-65e2-4d89-a69e-08cb3c9f34d2 # bogus
provider: some-keycloak
provider:
some-keycloak:
token-uri: https://some.bogus/token/uri
备选
您也可以只使用已经用于模拟 REST 资源的 mockserver
来模拟授权服务器并响应令牌请求。为此,您可以将 mockserver
配置为 src/test/resources/application.yml
中的 token-uri
或您用来分别为测试提供属性的任何内容。
备注
直接注入WebClient
在 bean 中提供 WebClient
的推荐方法是注入 WebClient.Builder
,这会得到 preconfigured by Spring Boot. This also guarantees, that the WebClient
in your test is configured exactly the same as in production. You can declare WebClientCustomizer
beans to configure this builder further。这就是它在我上面提到的展示存储库中实现的方式。
Bean overriding/prioritizing with @Primary
on a @Bean
inside a @Configuration
or @TestConfiguration
我也试过了,发现它并不总是像人们期望的那样工作,可能是因为 Spring 加载和实例化 bean 定义的顺序。例如,仅当 @TestConfiguration
是测试 class 中的 static nested
class 时才使用 ReactiveOAuth2AuthorizedClientManager
模拟,但如果它是 @Import
则不使用编辑。在接口上使用 static nested @TestConfiguration
并通过测试 class 实现它也不起作用。因此,为了避免将 static nested
class 放在我需要的每个集成测试中,我宁愿选择此处介绍的 @TestComponent
方法。
其他 OAuth 2.0 授权类型
我只针对 Client Credentials
授权类型测试了我的方法,但我认为它也可以针对其他授权类型进行调整或扩展。
我有一个小的 Spring Boot 2.2 批处理写入 OAuth2 REST API。
我已经能够在 https://medium.com/@asce4s/oauth2-with-spring-webclient-761d16f89cdd 之后配置 WebClient
并且它按预期工作。
@Configuration
public class MyRemoteServiceClientOauth2Config {
@Bean("myRemoteService")
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations,
new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
oauth.setDefaultClientRegistrationId("myRemoteService");
return WebClient.builder()
.filter(oauth)
.build();
}
}
但是,现在我想为我的批处理编写一个集成测试,并且我想避免使用 "real" 授权服务器来获取令牌:我不希望我的测试失败,如果外部服务器已关闭。我希望我的测试是 "autonomous"。
我正在调用的远程服务在我的测试期间被 mockserver
假服务所取代。
这种情况下的最佳做法是什么?
- 对我有用的是仅在使用
@Profile("!test")
和 运行 我的@ActiveProfiles("test")
测试之外启用上述配置。我还在我的测试中导入了一个特定于测试的配置:
@Configuration
@Profile("test")
public class BatchTestConfiguration {
@Bean("myRemoteService")
public WebClient webClientForTest() {
return WebClient.create();
}
}
但我觉得必须在我的生产配置中添加 @Profile("!test")
不是很好..
- 有没有一种 'cleaner' 方法来替换我正在使用的 WebClient bean,用一个无需先尝试获取令牌就可以调用我的假远程服务的方法?我试图在我的 webClientForTest bean 上放置一个
@Primary
,但它不起作用:生产 bean 仍然启用并且我得到一个异常:
No qualifying bean of type 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
这是生产 bean 需要的参数类型
- 作为测试的一部分,我是否需要启动一个伪造的授权服务器并配置 WebClient 以从中获取虚拟令牌?是否有图书馆尽可能开箱即用?
我和你的情况一样,找到了解决办法。首先,为了查看它的实际效果,我创建了一个 repository with a showcase implementation 包含下面解释的所有内容。
is there an 'cleaner' way to replace the WebClient bean I am using, by one that will call my fake remote service without trying to get a token first ?
我不会在您的测试中替换 WebClient
bean,而是用模拟替换 ReactiveOAuth2AuthorizedClientManager
bean。
为此,您必须稍微修改 MyRemoteServiceClientOauth2Config
。而不是使用现在已弃用的方法 UnAuthenticatedServerOAuth2AuthorizedClientRepository
you configure it this way (this is also more in line with the documented configuration on the Servlet-Stack):
@Configuration
public class MyRemoteServiceClientOauth2Config {
@Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2ClientCredentialsFilter =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(reactiveOAuth2AuthorizedClientManager);
oauth2ClientCredentialsFilter.setDefaultClientRegistrationId("myRemoteService");
return WebClient.builder()
.filter(oauth2ClientCredentialsFilter)
.build();
}
@Bean
public ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager(ReactiveClientRegistrationRepository clientRegistrations,
ReactiveOAuth2AuthorizedClientService authorizedClients) {
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, authorizedClients);
authorizedClientManager.setAuthorizedClientProvider(
new ClientCredentialsReactiveOAuth2AuthorizedClientProvider());
return authorizedClientManager;
}
}
然后你可以创建一个 ReactiveOAuth2AuthorizedClientManager
的模拟,它总是 returns 一个 Mono
of an OAuth2AuthorizedClient
像这样:
@TestComponent
@Primary
public class AlwaysAuthorizedOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
@Value("${spring.security.oauth2.client.registration.myRemoteService.client-id}")
String clientId;
@Value("${spring.security.oauth2.client.registration.myRemoteService.client-secret}")
String clientSecret;
@Value("${spring.security.oauth2.client.provider.some-keycloak.token-uri}")
String tokenUri;
/**
* {@inheritDoc}
*
* @return
*/
@Override
public Mono<OAuth2AuthorizedClient> authorize(final OAuth2AuthorizeRequest authorizeRequest) {
return Mono.just(
new OAuth2AuthorizedClient(
ClientRegistration
.withRegistrationId("myRemoteService")
.clientId(clientId)
.clientSecret(clientSecret)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.tokenUri(tokenUri)
.build(),
"some-keycloak",
new OAuth2AccessToken(TokenType.BEARER,
"c29tZS10b2tlbg==",
Instant.now().minus(Duration.ofMinutes(1)),
Instant.now().plus(Duration.ofMinutes(4)))));
}
}
最后 @Import
在你的测试中:
@SpringBootTest
@Import(AlwaysAuthorizedOAuth2AuthorizedClientManager.class)
class YourIntegrationTestClass {
// here is your test code
}
对应的src/test/resources/application.yml
是这样的:
spring:
security:
oauth2:
client:
registration:
myRemoteService:
authorization-grant-type: client_credentials
client-id: test-client
client-secret: 6b30087f-65e2-4d89-a69e-08cb3c9f34d2 # bogus
provider: some-keycloak
provider:
some-keycloak:
token-uri: https://some.bogus/token/uri
备选
您也可以只使用已经用于模拟 REST 资源的 mockserver
来模拟授权服务器并响应令牌请求。为此,您可以将 mockserver
配置为 src/test/resources/application.yml
中的 token-uri
或您用来分别为测试提供属性的任何内容。
备注
直接注入WebClient
在 bean 中提供 WebClient
的推荐方法是注入 WebClient.Builder
,这会得到 preconfigured by Spring Boot. This also guarantees, that the WebClient
in your test is configured exactly the same as in production. You can declare WebClientCustomizer
beans to configure this builder further。这就是它在我上面提到的展示存储库中实现的方式。
Bean overriding/prioritizing with @Primary
on a @Bean
inside a @Configuration
or @TestConfiguration
我也试过了,发现它并不总是像人们期望的那样工作,可能是因为 Spring 加载和实例化 bean 定义的顺序。例如,仅当 @TestConfiguration
是测试 class 中的 static nested
class 时才使用 ReactiveOAuth2AuthorizedClientManager
模拟,但如果它是 @Import
则不使用编辑。在接口上使用 static nested @TestConfiguration
并通过测试 class 实现它也不起作用。因此,为了避免将 static nested
class 放在我需要的每个集成测试中,我宁愿选择此处介绍的 @TestComponent
方法。
其他 OAuth 2.0 授权类型
我只针对 Client Credentials
授权类型测试了我的方法,但我认为它也可以针对其他授权类型进行调整或扩展。