微服务实战:如何测试基于OAuth认证的微服务

背景

正如 微服务实战:基于Spring Cloud Gateway + AWS Cognito 的BFF案例 一文中所介绍的,我司的微服务群采用了Spring Cloud Gateway作为API认证网关,利用Spring Security为API认证网关和后端微服务提供了OAuth认证功能。

我们想做什么

  • 想测试单个微服务
  • 想测试OAuth认证流程

我们不想做什么

  • 不想为了测试部署所有的微服务
  • 不想在测试中连接真实的OAuth认证服务器

相关测试类型

当我们尝试测试与其他服务存在通信的微服务程序时,我们可以做以下两件事之一:

  1. 部署所有微服务并执行端到端测试
  2. 在单元/集成测试中模拟其他微服务

两者各有其优缺点。首先,对于端到端测试方式

优点

  • 模拟生产环境
  • 测试服务之间的真实通信

缺点

  • 要测试一个微服务,我们必须部署多个微服务、多个数据库等
  • 执行测试的环境将被锁定,专门为一组测试所占用(即在此期间没有其他人能够运行测试)
  • 测试执行时间很久
  • 很晚才能得到反馈
  • 极难调试(debug)

其次,对于单体/集成测试方式

优点

  • 非常快的反馈
  • 没有基础设施要求

缺点

  • 被测试的微服务的开发人员负责自己打桩(Stub),因此测试桩与真正的实现可能有差异
  • 虽然测试通过但上生产环境之后仍然可能出错

可以看出这两种测试其实是相辅相成,互相补充的。在项目工期和资源允许的情况下,两者都可以安排上。但在人员匮乏,工期紧张的时候,我个人倾向于后者。原因是我们可以快速实施测试,并快速得到反馈,缓解工期的紧张;另外还可以采取一些技术手段尽量避免它带来的缺点。比如:优先开发基于OAS(Open API Specification)的API规格,API Provider和Consumer都基于共同的OAS进行开发。这样可以减少测试桩和真正实现之间的差异。

本文的讨论将侧重于后者。

API网关的测试

Spring Security文档中,建议采用WebTestClient来测试基于Webflux的响应式程序。用它可以很方便的设置我们测试所需的各种认证信息。在如下测试类上使用了@AutoConfigureWebTestClient进行自动配置。

扫描二维码关注公众号,回复: 13471492 查看本文章

另外,com.github.tomakehurst.wiremock.client.WireMock可以帮助我们快速设置测试桩(Stub),在如下测试类上使用了@AutoConfigureWireMock(port = 0)自动配置,此时port = 0设置的端口是随机的,端口值可以通过${wiremock.server.port}参数获取。

另外,我们指定了@ActiveProfiles("test"),所以在执行这个测试类时application-test.yaml中的配置会生效。

@AutoConfigureWebTestClient
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
@ActiveProfiles("test")
class ApiGatewayApplicationTests {

    @Autowired
    private WebTestClient client;
 
    // ...
}
复制代码

为了在测试时使用Stub的URI,我们需要改造一下Controller,先将URI配置到application.yaml中,然后通过@Value("${order-service-uri}")获取。

@RestController
public class CompositionController {

    @Value("${order-service-uri}")
    private String orderServiceUri;

    @Value("${storage-service-uri}")
    private String storageServiceUri;

    @GetMapping("/composition/{id}")
    public Mono<? extends ResponseEntity<?>> proxy(@PathVariable Integer id, ProxyExchange<?> proxy) {

        return proxy.uri(orderServiceUri + "/api/order/get/" + id)
            .get(resp -> ResponseEntity.status(resp.getStatusCode())
                .body(resp.getBody()))
            .flatMap(re1 -> proxy.uri(storageServiceUri + "/api/storage/get/" + id)
                .get(resp -> ResponseEntity.status(resp.getStatusCode())
                    .body(Map.of("order",re1.getBody(),"storage",resp.getBody()))));
    }

}
复制代码

同时,在如下application-test.yaml中,将上述URI设置为Stub的地址,这里通过${wiremock.server.port}参数获取随机端口。

account-service-uri: http://localhost:${wiremock.server.port}
order-service-uri: http://localhost:${wiremock.server.port}
storage-service-uri: http://localhost:${wiremock.server.port}
复制代码

另外,在application-test.yaml中还要做如下配置,首先我们不需要将Token传递给Stub,所以将spring.cloud.gateway.default-filters的设置清空。另外,需要注册一个名为test的客户端,这里不需要真实的配置信息。

spring:
  cloud:
    gateway:
      default-filters: # set to empty
  security:
    oauth2:
      client:
        provider:
          test: # add for testing
            issuerUri: https://cognito-idp.<region-id>.amazonaws.com/<region-id>_<user-pool-id>
            user-name-attribute: username
        registration:
          test:
            client-id: dummy-client-id
            client-secret: dummy-client-secret
            client-name: scg-cognito-sample-user-pool
            provider: cognito
            scope: openid
            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
            authorization-grant-type: authorization_code
复制代码

然后我们就可以在测试类中设置测试桩(Stub)了。下面的Stub会在我们访问http://localhost:${wiremock.server.port}/api/storage/get/123时,返回我们预设的 Header 和 Body。

@BeforeAll
static void init() {
    //Stubs
    stubFor(get(urlEqualTo("/api/storage/get/123"))
        .willReturn(aResponse()
            .withBody("{"id":101,"commodityCode":"123","count":100}")
            .withHeader("Content-Type", "application/json")));
    // ...
}
复制代码

启动测试应用程序上下文后,我们要向API网关发出经过身份验证的请求。 我们可以使用@WithMockUser(roles = "ADMIN")之类的注解或者mutateWith方法,它可以设置我们需要的任何属性。下面的例子中使用了mutateWith方法构造了一个ID Token,使得我们可以通过API网关的OAuth认证,如果不使用mutateWith方法,API网关会返回401 Unauthorized

@Test
void testGetComposition() {
    client.mutateWith(mockOidcLogin()
            .idToken(builder -> builder.subject("Subject A")))
        .get().uri("/composition/123").exchange()
        .expectStatus().is2xxSuccessful();
}
复制代码

我们还可以为测试用户添加任何权限(Authority),如下所示,我们增加了ROLE_account.access权限,这样API网关可以通过hasRole("account.access")的鉴权校验,否则API网关会返回403 Forbidden

@Test
void testGetHomeAuthenticated() {

    client.mutateWith(mockOidcLogin()
            .idToken(builder -> builder.subject("Subject A"))
            .authorities(new SimpleGrantedAuthority("ROLE_account.access"))
        )
        .get().uri("/api/account/whoami").exchange()
        .expectStatus().is2xxSuccessful()
        .expectBody().jsonPath("$.['account.access']").isEqualTo("/api/account/**");
}
复制代码

微服务的测试

对于后端微服务的测试就相对简单一些了,我们只需要在微服务上验证JWT令牌。 我们在Account服务中使用 Spring Security 5.2 中引入的新 jwt() RequestPostProcessor 来轻松更改 JWT 特性。

@SpringBootTest
@AutoConfigureMockMvc
class AccountControllerTest {

    @Autowired
    private MockMvc mockmvc;

    private final String base_url = "/api/account";

    @Test
    void whoami() throws Exception {
        mockmvc.perform(get(base_url + "/whoami").with(jwt().jwt(builder -> builder.subject("Subject A"))))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Subject A"));

    }

}
复制代码

总结

本文着重介绍了微服务的单体/集成测试方法,我们使用WireMock设置了测试桩,并使用mutateWithjwt()设置了OAuth认证所需的各种信息。如果对你有所帮助,请点赞订阅分享,感谢!

相关文章

参考链接

猜你喜欢

转载自juejin.im/post/7037294541983973384