一、简介
在本教程中,我们将了解如何使用Spring Cloud Gateway 在将响应主体发送回客户端之前检查和/或修改响应主体。
2. Spring Cloud Gateway 快速回顾
Spring Cloud Gateway,简称SCG,是Spring Cloud 系列的一个子项目,它提供了一个构建在响应式Web 堆栈之上的API 网关。我们已经在之前的教程中介绍了它的基本用法,所以我们不会在这里讨论这些方面。
相反,这次我们将关注在围绕API 网关设计解决方案时不时出现的特定使用场景:如何在将后端响应有效负载发送回客户端之前对其进行处理?
以下是我们可能会使用此功能的一些情况的列表:
保持与现有客户端的兼容性,同时允许后端发展
从响应中屏蔽某些字段以遵守PCI 或GDPR 等法规
在更实际的情况下,满足这些要求意味着我们需要实现一个过滤器来处理后端响应。由于过滤器是SCG 中的核心概念,我们需要做的就是支持响应处理,实现一个应用所需转换的自定义过滤器。
此外,一旦我们创建了过滤器组件,我们就可以将它应用于任何声明的路由。
3. 实现数据清理过滤器
为了更好地说明响应正文操作的工作原理,让我们创建一个简单的过滤器来屏蔽基于JSON 的响应中的值。例如,给定一个JSON 具有一个名为“ssn”的字段:
{ "name" : "John Doe", "ssn" : "123-45-9999", "account" : "9999888877770000" }
我们想用固定值替换它们的值,从而防止数据泄漏:
{ "name" : "John Doe", "ssn" : "****", "account" : "9999888877770000" }
3.1。实现GatewayFilterFactory
顾名思义,GatewayFilterFactory
是给定时间过滤器的工厂。在启动时,Spring 会查找任何实现此接口的@Component
-annotated 类。然后它会构建一个可用过滤器的注册表,我们可以在声明路由时使用它:
spring: cloud: gateway: routes: - id: rewrite_with_scrub uri: ${rewrite.backend.uri:http://example.com} predicates: - Path=/v1/customer/** filters: - RewritePath=/v1/customer/(?<segment>.*),/api/$\{segment} - ScrubResponse=ssn,***
请注意,当使用这种基于配置的方法来定义路由时,根据SCG 的预期命名约定来命名我们的工厂非常重要:FilterNameGatewayFilterFactory
。考虑到这一点,我们将把工厂命名为ScrubResponseGatewayFilterFactory.
SCG 已经有几个实用程序类,我们可以使用它们来实现这个工厂。在这里,我们将使用开箱即用的过滤器常用的一个:AbstractGatewayFilterFactory<T>
,一个模板化的基类,其中T 代表与我们的过滤器实例关联的配置类。在我们的例子中,我们只需要两个配置属性:
fields
:用于匹配字段名称的正则表达式replacement
:将替换原始值的字符串
我们必须实现的关键方法是apply()
。SCG 为使用我们过滤器的每个路由定义调用此方法。例如,在上面的配置中,apply()
只会被调用一次,因为只有一个路由定义。
在我们的例子中,实现很简单:
@Override public GatewayFilter apply(Config config) { return modifyResponseBodyFilterFactory .apply(c -> c.setRewriteFunction(JsonNode.class, JsonNode.class, new Scrubber(config))); }
在这种情况下非常简单,因为我们使用了另一个内置过滤器ModifyResponseBodyGatewayFilterFactory
,我们将所有与正文解析和类型转换相关的繁重工作委托给它。我们使用构造函数注入来获取这个工厂的一个实例,并且在apply(),
我们将创建GatewayFilter
实例的任务委托给它。
这里的关键点是使用apply()
方法变体,该变体不是获取配置对象,而是期望Consumer
用于配置。同样重要的是,此配置是ModifyResponseBodyGatewayFilterFactory
配置。这个配置对象提供了我们在代码中调用的setRewriteFunction()
方法。
3.2.使用setRewriteFunction()
现在,让我们更深入地了解setRewriteFunction().
此方法接受三个参数:两个类(输入和输出)和一个可以从传入类型转换为传出类型的函数。在我们的例子中,我们没有转换类型,所以输入和输出都使用同一个类:JsonNode
。此类来自Jackson 库,位于用于表示JSON 中不同节点类型(例如对象节点、数组节点等)的类层次结构的最顶端。使用JsonNode
作为输入/输出类型允许我们处理任何有效的JSON 有效负载,这在本例中是我们想要的。
对于转换器类,我们传递了一个Scrubber
的实例,它在其apply()
方法中实现了所需的RewriteFunction
接口:
public static class Scrubber implements RewriteFunction<JsonNode,JsonNode> { // ... fields and constructor omitted @Override public Publisher<JsonNode> apply(ServerWebExchange t, JsonNode u) { return Mono.just(scrubRecursively(u)); } // ... scrub implementation omitted }
传递给apply()
的第一个参数是当前的ServerWebExchange
,它使我们能够访问到目前为止的请求处理上下文。我们不会在这里使用它,但很高兴知道我们有这种能力。下一个参数是接收到的正文,已经转换为课堂内通知。
预期的回报是通知类的实例的Publisher
者。所以,只要我们不做任何阻塞I/O 操作,我们就可以在rewrite 函数内部做一些复杂的工作。
3.3.Scrubber
实施
所以,既然我们知道了重写函数的合约,让我们最终实现我们的清理器逻辑。在这里,我们假设有效负载相对较小,因此我们不必担心存储接收到的对象的内存需求。
它的实现只是递归遍历所有节点,寻找与配置模式匹配的属性并替换掩码的相应值:
public static class Scrubber implements RewriteFunction<JsonNode,JsonNode> { // ... fields and constructor omitted private JsonNode scrubRecursively(JsonNode u) { if ( !u.isContainerNode()) { return u; } if (u.isObject()) { ObjectNode node = (ObjectNode)u; node.fields().forEachRemaining((f) -> { if ( fields.matcher(f.getKey()).matches() && f.getValue().isTextual()) { f.setValue(TextNode.valueOf(replacement)); } else { f.setValue(scrubRecursively(f.getValue())); } }); } else if (u.isArray()) { ArrayNode array = (ArrayNode)u; for ( int i = 0 ; i < array.size() ; i++ ) { array.set(i, scrubRecursively(array.get(i))); } } return u; } }
4. 测试
我们在示例代码中包含了两个测试:一个简单的单元测试和一个集成测试。第一个只是一个常规的JUnit 测试,用作洗涤器的健全性检查。集成测试更有趣,因为它说明了SCG 开发环境中的有用技术。
首先,存在提供可以发送消息的实际后端的问题。一种可能性是使用像Postman 或类似工具这样的外部工具,这会给典型的CI/CD 场景带来一些问题。相反,我们将使用JDK 鲜为人知的HttpServer
类,它实现了一个简单的HTTP 服务器。
@Bean public HttpServer mockServer() throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(0),0); server.createContext("/customer", (exchange) -> { exchange.getResponseHeaders().set("Content-Type", "application/json"); byte[] response = JSON_WITH_FIELDS_TO_SCRUB.getBytes("UTF-8"); exchange.sendResponseHeaders(200,response.length); exchange.getResponseBody().write(response); }); server.setExecutor(null); server.start(); return server; }
该服务器将在/customer
处处理请求并返回我们测试中使用的固定JSON 响应。请注意,返回的服务器已经启动,并将在随机端口监听传入的请求。我们还指示服务器创建一个新的默认Executor
来管理用于处理请求的线程
其次,我们以编程方式创建一个包含我们的过滤器的路由@Bean
。这相当于使用配置属性构建路由,但允许我们完全控制测试路由的所有方面:
@Bean public RouteLocator scrubSsnRoute( RouteLocatorBuilder builder, ScrubResponseGatewayFilterFactory scrubFilterFactory, SetPathGatewayFilterFactory pathFilterFactory, HttpServer server) { int mockServerPort = server.getAddress().getPort(); ScrubResponseGatewayFilterFactory.Config config = new ScrubResponseGatewayFilterFactory.Config(); config.setFields("ssn"); config.setReplacement("*"); SetPathGatewayFilterFactory.Config pathConfig = new SetPathGatewayFilterFactory.Config(); pathConfig.setTemplate("/customer"); return builder.routes() .route("scrub_ssn", r -> r.path("/scrub") .filters( f -> f .filter(scrubFilterFactory.apply(config)) .filter(pathFilterFactory.apply(pathConfig))) .uri("http://localhost:" + mockServerPort )) .build(); }
最后,这些bean 现在是@TestConfiguration
的一部分,我们可以将它们与WebTestClient
一起注入到实际测试中。实际测试使用这个WebTestClient
来驱动旋转的SCG 和后端:
@Test public void givenRequestToScrubRoute_thenResponseScrubbed() { client.get() .uri("/scrub") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus() .is2xxSuccessful() .expectHeader() .contentType(MediaType.APPLICATION_JSON) .expectBody() .json(JSON_WITH_SCRUBBED_FIELDS); }
5. 结论
在本文中,我们展示了如何访问后端服务的响应主体并使用Spring Cloud Gateway 库对其进行修改。
0 评论