在测试中替换 OAuth2 WebClient [英] replacing an OAuth2 WebClient in a test

查看:26
本文介绍了在测试中替换 OAuth2 WebClient的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个小的 Spring Boot 2.2 批处理,可以写入 OAuth2 REST API.

I have a small Spring Boot 2.2 batch that writes to an OAuth2 REST API.

我已经能够按照 WebClient">https://medium.com/@asce4s/oauth2-with-spring-webclient-761d16f89cdd 并且它按预期工作.

I have been able to configure the WebClient following https://medium.com/@asce4s/oauth2-with-spring-webclient-761d16f89cdd and it works as expected.

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

    }

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

However, now I would like to write an integration test for my batch, and I would like to avoid using the "real" authorization server to get a token : I don't want my test to fail if an external server is down. I want my test to be "autonomous".

我正在调用的远程服务在我的测试中被一个 mockserver 假的服务取代.

The remote service I am calling is replaced by a mockserver fake one during my tests.

这种情况下的最佳实践是什么?

What is the best practice in that case ?

  • 对我有用的是仅在使用 @Profile("!test") 的测试之外启用上述配置,并使用 @ActiveProfiles("test") 运行我的测试>.我还在我的测试中导入了一个测试特定的配置:
  • What works for me is to enable above config only outside of tests with @Profile("!test") and run my tests with @ActiveProfiles("test"). I also import a test specific config in my test :
    @Configuration
    @Profile("test")
    public class BatchTestConfiguration {

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

            return WebClient.create();
        }

    }

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

But I feel having to add @Profile("!test") on my production config is not great..

  • 是否有一种更干净"的方法来替换我正在使用的 WebClient bean,它会调用我的假远程服务而不先尝试获取令牌?我试图在我的 webClientForTest bean 上放置一个 @Primary,但它不起作用:生产 bean 仍然被启用并且我得到一个异常:
  • 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 ? I tried to put a @Primary on my webClientForTest bean, but it doesn't work : the production bean still gets enabled and I get an exception :

没有符合条件的 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository 类型的 bean

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

生产bean需要的参数类型

which is the parameter type the production bean needs

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

推荐答案

我和你的情况一样,找到了解决方案.首先,为了看到它的实际效果,我创建了一个带有展示实现的 存储库 下面解释的所有内容.

I was in the same situation as you and found a solution. First off, to see it in action, I have created a repository with a showcase implementation of everything that is explained below.

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

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 你这样配置(这也更符合Servlet-Stack 上的文档配置):

I would not replace the WebClient bean in your test, but rather replace the ReactiveOAuth2AuthorizedClientManager bean with a mock. For this to work you have to slightly modify your MyRemoteServiceClientOauth2Config. Instead of using the now deprecated approach with an 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 的模拟,它总是返回一个 OAuth2AuthorizedClient 像这样:

Then you can create a mock of ReactiveOAuth2AuthorizedClientManager that always returns a Mono of an OAuth2AuthorizedClient like this:

@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 在你的测试中:

And finally @Import that in your test:

@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


替代方案

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


Alternative

You could also just use the same mockserver you are already using to mock your REST-Resource, to also mock the Authorization server and respond to the token request. For this to work, you would configure the mockserver as the token-uri in the src/test/resources/application.yml or whatever you are using to provide properties to your test respectively.

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

The recommended way of providing a WebClient in your beans is by injecting WebClient.Builder, which gets 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. This is the way it is implemented in my showcase repository mentioned above.

我也尝试过,发现它并不总是像人们期望的那样工作,可能是因为 Spring 加载和实例化 bean 定义的顺序.例如,ReactiveOAuth2AuthorizedClientManager 模拟仅在 @TestConfiguration 是测试类中的 static nested 类时才使用,如果它是 则不使用>@Import 编辑.在接口上使用 static 嵌套 @TestConfiguration 并使用测试类实现它也不起作用.因此,为了避免将 静态嵌套 类放在我需要的每个集成测试中,我宁愿选择此处介绍的 @TestComponent 方法.

I have tried that too and found that it does not always work the way one would expect, probably because of the order in which Spring loads and instantiates the bean definitions. For instance, the ReactiveOAuth2AuthorizedClientManager mock is only used if the @TestConfiguration is a static nested class inside the test class but not if it is @Imported. Having the static nested @TestConfiguration on an interface and implement that with the test class also does not work. So, to avoid putting that static nested class on every integration test I need it in, I rather opt for the @TestComponent approach presented here.

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

I only tested my approach for the Client Credentials Grant Type, but I think it could also be adapted or expanded for other Grant Types as well.

这篇关于在测试中替换 OAuth2 WebClient的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆