当前位置: 首页 > 知识库问答 >
问题:

在测试中替换OAuth2 WebClient

卫高谊
2023-03-14

我有一个小型的spring boot 2.2批处理,它编写了一个OAuth2 REST API。

我已经能够按照https://medium.com/@asce4s/oauth2-with--webclient-761d16f89cdd配置webclient并且它能够正常工作。

java prettyprint-override">    @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();
        }

    }

但是,现在我想为我的批处理编写一个集成测试,我想避免使用“真正的”授权服务器来获得一个令牌:如果一个外部服务器宕机,我不希望我的测试失败。我希望我的测试是“自主”的。

在测试期间,我调用的远程服务被mockserver假服务替换。

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

  • 对我有效的方法是仅在使用@profile(“!test”)的测试之外启用上述配置,并使用@activeprofiles(“test”)运行我的测试。我还在测试中导入特定于测试的配置:
    @Configuration
    @Profile("test")
    public class BatchTestConfiguration {

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

            return WebClient.create();
        }

    }

但我觉得必须在我的生产配置中添加@profile(“!test”)并不是很好。

  • 有没有一种‘更干净’的方法来替换我正在使用的WebClient bean,它将调用我的假远程服务而不首先尝试获取令牌?我试图将@primary放在webClientForTest bean上,但这不起作用:生产bean仍然启用,而我得到一个异常:

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

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

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

共有1个答案

陈松
2023-03-14

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

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

我不会在您的测试中替换WebClientbean,而是将ReactiveOAuth2AuthorizedClientManagerbean替换为一个mock。为此,您必须稍微修改MyRemoteServiceClientoAuth2Config。与使用unauthenticatedserveroauth2authorizedclientrepository的方法相比,您可以这样配置它(这也更符合servlet堆栈上的文档配置):

@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的模拟,该模拟始终返回Oauth2AuthorizedClientMono,如下所示:

@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或用于分别为测试提供属性的任何文件。

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

我也试过了,发现它并不总是按照预期的方式工作,可能是因为spring加载和实例化bean定义的顺序。例如,ReactiveOAuth2AuthorizedClientManager模拟仅在@TestConfiguration是测试类内部的静态嵌套类时才使用,但如果它是@ImportED则不使用。在接口上使用静态嵌套的@testconfiguration并用测试类实现它也是不起作用的。因此,为了避免将静态嵌套类放在我需要的每个集成测试中,我宁愿选择这里介绍的@TestComponent方法。

我只测试了client credentials授予类型的方法,但我认为它也可以适用于其他授予类型。

 类似资料:
  • 使用ScalaTest,我想替换测试用例中的函数实现。我的用例: 我想写一个单元测试,但我不希望此测试用例依赖于测试运行的实际年份。 在动态语言中,我经常使用一个可以替换函数实现以返回固定值的构造。 我希望我的测试用例更改始终返回2014,无论实际年份是什么。 我发现了几个模拟库(Mockito、ScalaMock等等),但它们都只能创建新的模拟对象。它们似乎都无法取代方法的实现。 有办法做到吗?

  • 我试图用JUnit在测试会话期间正确使用Mockito来代替存根类。不幸的是,在网络上有很多关于Mockito的教程,但关于存根方法的教程较少,我想学习这项技术。 此测试由Mockito进行: 为了澄清这些是所涉及的类: 1) 控制器 2)回购接口: 3) 回购协议: 4) 验证器: 5) RecentyExceptionHandler

  • 我正在使用一个具有许多不同库依赖关系的gradle项目,并使用新的清单合并。在我的

  • 我正在使用此Composer包进行需要此包提供的功能的开发。寻找替代方案,我发现这是我最好的选择,因为可用的替代方案太简单了,无法在我处理的时间限制下实现。 现在,我已经在运行PHP 5.5的本地机器(在LinuxWindows 10的子系统上)和运行PHP 5.6的个人服务器上测试了它,但是正式服运行PHP 5.4,由于原因无法升级。 首先我犯了这个错误: 在寻找解决方案时,我遇到了这个问题,因

  • 我很难理解Groovy单元测试中的Spock交互。 第二个测试尝试修改mock,使其为方法返回。然后,我们期望(它委托给)返回。但是,测试失败如下: Foospec:说再见:不满足26个条件 因为仍然等于。 我可能仍然在Mockito模式下思考,并假设交互等效于表达式,以后的交互将覆盖以前的交互。 是否有一种简单的方法使用Spock在方法中声明交互,然后在测试用例中重写该交互?还是需要删除方法,并

  • 是否可以替换DI容器中的私有服务?我需要在我的测试环境中这样做,这样我就可以运行集成测试,但仍然可以模拟对外部API的HTTP调用。 例如,我有这样的代码来设置HttpClientInterface的模拟: 我已经尝试使用下面的配置将HttpClientInterface定义为我的测试环境的公共服务,但这不起作用,因为它是不可实例化的(它是一个接口)。