2. Spring Cloud Gateway 快速回顾
Spring Cloud Gateway(简称 SCG)是 Spring Cloud 家族下的子项目,提供基于响应式 Web 栈的 API 网关。基础用法已在之前的教程中覆盖,本文不再赘述。
本文聚焦一个常见场景:如何在返回客户端前处理后端服务的响应载荷?
典型应用场景包括:
- 保持客户端兼容性同时允许后端演进
- 遵守 PCI/GDPR 等法规要求,屏蔽敏感字段
实现这些需求的核心是自定义过滤器。作为 SCG 的核心概念,我们只需实现一个自定义过滤器并应用到指定路由即可。
3. 实现数据脱敏过滤器
通过一个 JSON 响应脱敏的示例说明响应体修改原理。给定包含 ssn
字段的 JSON:
{
"name" : "John Doe",
"ssn" : "123-45-9999",
"account" : "9999888877770000"
}
目标是将敏感值替换为固定字符:
{
"name" : "John Doe",
"ssn" : "****",
"account" : "9999888877770000"
}
3.1. 实现 GatewayFilterFactory
GatewayFilterFactory 是过滤器的工厂类。Spring 启动时会扫描所有实现该接口的 @Component
类,构建可用过滤器注册表。路由配置中可直接使用:
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 会对使用该过滤器的每个路由调用此方法。实现如下:
@Override
public GatewayFilter apply(Config config) {
return modifyResponseBodyFilterFactory
.apply(c -> c.setRewriteFunction(JsonNode.class, JsonNode.class, new Scrubber(config)));
}
这里通过内置的 ModifyResponseBodyGatewayFilterFactory
委托处理响应体解析和类型转换。关键技巧是:
- 使用
Consumer
配置而非配置对象 - 调用
setRewriteFunction()
设置转换逻辑
3.2. 使用 setRewriteFunction()
深入分析 setRewriteFunction()
方法:
setRewriteFunction(Class<?> inClass, Class<?> outClass, RewriteFunction<?, ?> rewriteFunction)
参数说明:
inClass
/outClass
:输入/输出类型(本例均为JsonNode
)rewriteFunction
:实现转换逻辑的函数
选择 JsonNode
的原因:
- Jackson 库的顶层 JSON 节点类型
- 支持处理对象、数组等任意 JSON 结构
转换器需实现 RewriteFunction
接口:
public static class Scrubber implements RewriteFunction<JsonNode,JsonNode> {
// ... 字段和构造函数省略
@Override
public Publisher<JsonNode> apply(ServerWebExchange exchange, JsonNode body) {
return Mono.just(scrubRecursively(body));
}
// ... 脱敏实现省略
}
方法参数:
ServerWebExchange
:提供请求处理上下文(本例未使用)body
:已解析的响应体
返回 Publisher<JsonNode>
,支持异步非阻塞处理。
3.3. Scrubber 实现
脱敏逻辑采用递归遍历 JSON 节点,假设载荷较小可直接内存处理:
public static class Scrubber implements RewriteFunction<JsonNode,JsonNode> {
// ... 字段和构造函数省略
private JsonNode scrubRecursively(JsonNode node) {
if (!node.isContainerNode()) {
return node;
}
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode)node;
objectNode.fields().forEachRemaining(entry -> {
if (fields.matcher(entry.getKey()).matches() && entry.getValue().isTextual()) {
entry.setValue(TextNode.valueOf(replacement));
} else {
entry.setValue(scrubRecursively(entry.getValue()));
}
});
} else if (node.isArray()) {
ArrayNode arrayNode = (ArrayNode)node;
for (int i = 0; i < arrayNode.size(); i++) {
arrayNode.set(i, scrubRecursively(arrayNode.get(i)));
}
}
return node;
}
}
处理逻辑:
- 非容器节点直接返回
- 对象节点:检查字段名匹配且值为文本时替换,否则递归处理子节点
- 数组节点:递归处理每个元素
4. 测试
示例代码包含单元测试和集成测试。集成测试展示了 SCG 开发的实用技巧:
4.1. 模拟后端服务
使用 JDK 内置的 HttpServer
模拟后端,避免依赖外部工具:
@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(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, response.length);
exchange.getResponseBody().write(response);
});
server.setExecutor(null);
server.start();
return server;
}
关键点:
- 监听随机端口(
0
表示自动分配) - 返回固定 JSON 响应
- 使用默认线程池处理请求
4.2. 编程式路由配置
通过 Java 配置创建测试路由,完全控制路由行为:
@Bean
public RouteLocator scrubSsnRoute(
RouteLocatorBuilder builder,
ScrubResponseGatewayFilterFactory scrubFilterFactory,
SetPathGatewayFilterFactory pathFilterFactory,
HttpServer server) {
int mockServerPort = server.getAddress().getPort();
ScrubResponseGatewayFilterFactory.Config scrubConfig = new ScrubResponseGatewayFilterFactory.Config();
scrubConfig.setFields("ssn");
scrubConfig.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(scrubConfig))
.filter(pathFilterFactory.apply(pathConfig)))
.uri("http://localhost:" + mockServerPort))
.build();
}
配置步骤:
- 获取模拟服务器端口
- 配置脱敏过滤器(替换
ssn
字段为*
) - 配置路径重写过滤器(将
/scrub
映射到/customer
) - 构建指向模拟服务的路由
4.3. 集成测试用例
使用 WebTestClient
验证端到端行为:
@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);
}
验证点:
- 响应状态码 2xx
- 响应头
Content-Type: application/json
- 响应体符合脱敏后的 JSON 结构
5. 结论
本文展示了通过 Spring Cloud Gateway 访问并修改后端服务响应体的完整方案。核心技巧是:
- 继承
AbstractGatewayFilterFactory
实现自定义过滤器 - 利用
ModifyResponseBodyGatewayFilterFactory
处理响应体转换 - 通过递归遍历实现复杂 JSON 结构的修改
这种模式在数据脱敏、API 兼容性维护等场景特别实用,简单粗暴地解决了响应体处理的常见需求。