在测试中替换 OAuth2 WebClient

Vin*_*t F 3 spring-boot spring-webclient

我有一个小型 Spring Boot 2.2 批次,用于写入 OAuth2 REST API。

我已经能够配置以下WebClienthttps://medium.com/@asce4s/oauth2-with-spring-webclient-761d16f89cdd并且它按预期工作。

    @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();
        }

    }
Run Code Online (Sandbox Code Playgroud)

但是,现在我想为我的批次编写集成测试,并且我想避免使用“真实”授权服务器来获取令牌:如果外部服务器关闭,我不希望我的测试失败。我希望我的测试是“自主的”。

在我的测试过程中,我调用的远程服务被mockserver假的服务所取代。

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

  • 对我有用的是仅在测试之外启用上述配置@Profile("!test")并使用 运行我的测试@ActiveProfiles("test")。我还在测试中导入了测试特定配置:
    @Configuration
    @Profile("test")
    public class BatchTestConfiguration {

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

            return WebClient.create();
        }

    }
Run Code Online (Sandbox Code Playgroud)

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

  • 有没有一种“更干净”的方法来替换我正在使用的 WebClient bean,通过调用我的假远程服务而无需先尝试获取令牌的方法?我尝试@Primary在我的 webClientForTest bean 上添加一个,但它不起作用:生产 bean 仍然启用,并且出现异常:

没有类型为“org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository”的合格 bean

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

  • 作为测试的一部分,我是否需要启动一个假授权服务器并配置 WebClient 以从中获取虚拟令牌?是否有一个库可以尽可能开箱即用地提供此功能?

clo*_*ken 8

我和你的情况一样,找到了解决办法。首先,为了查看它的实际效果,我创建了一个存储库,其中展示了下面解释的所有内容的实现。

有没有一种“更干净”的方法来替换我正在使用的 WebClient bean,通过调用我的假远程服务而无需先尝试获取令牌的方法?

我不会替换WebClient您测试中的 bean,而是ReactiveOAuth2AuthorizedClientManager用模拟替换 bean。为此,您必须稍微修改您的MyRemoteServiceClientOauth2Config. 您可以通过这种方式配置它,而不是使用现在已弃用的方法UnAuthenticatedServerOAuth2AuthorizedClientRepository(这也更符合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;
    }
}
Run Code Online (Sandbox Code Playgroud)

ReactiveOAuth2AuthorizedClientManager然后你可以创建一个总是返回 aMono的模拟,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)))));
    }
}
Run Code Online (Sandbox Code Playgroud)

最后@Import在你的测试中:

@SpringBootTest
@Import(AlwaysAuthorizedOAuth2AuthorizedClientManager.class)
class YourIntegrationTestClass {

  // here is your test code

}
Run Code Online (Sandbox Code Playgroud)

对应的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
Run Code Online (Sandbox Code Playgroud)

选择

您还可以使用已经使用的相同内容mockserver来模拟 REST 资源,也可以模拟授权服务器并响应令牌请求。为此,您可以将或任何您用来分别为测试提供属性的内容配置mockserver为。token-urisrc/test/resources/application.yml


笔记

WebClient直接注入

WebClient在 bean 中提供 a 的推荐方法是通过注入WebClient.Builder,它由 Spring Boot预先配置。这也保证了WebClient测试中的配置与生产中的配置完全相同。您可以声明WebClientCustomizerbean 来进一步配置此构建器。这就是它在上面提到的我的展示存储库中实现的方式。

Bean 覆盖/优先级@Primary@Beana@Configuration@TestConfiguration

我也尝试过,发现它并不总是按照人们期望的方式工作,可能是因为 Spring 加载和实例化 bean 定义的顺序。例如,只有当它是测试类中的类ReactiveOAuth2AuthorizedClientManager时才使用模拟,但如果它是ed 则不使用模拟。在接口上使用并使用测试类实现它也是行不通的。因此,为了避免将该类放在我需要的每个集成测试中,我宁愿选择此处介绍的方法。@TestConfigurationstatic nested@Importstatic nested @TestConfigurationstatic nested@TestComponent

其他 OAuth 2.0 授权类型

我只针对Client Credentials资助类型测试了我的方法,但我认为它也可以针对其他资助类型进行调整或扩展。