在测试中替换 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 假服务所取代。

这种情况下的最佳做法是什么?

    @Configuration
    @Profile("test")
    public class BatchTestConfiguration {

        @Bean("myRemoteService")
        public WebClient webClientForTest() {

            return WebClient.create();
        }

    }

但我觉得必须在我的生产配置中添加 @Profile("!test") 不是很好..

No qualifying bean of type 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository

这是生产 bean 需要的参数类型

我和你的情况一样,找到了解决办法。首先,为了查看它的实际效果,我创建了一个 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 授权类型测试了我的方法,但我认为它也可以针对其他授权类型进行调整或扩展。