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 委托处理响应体解析和类型转换。关键技巧是:

  1. 使用 Consumer 配置而非配置对象
  2. 调用 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;
    }
}

处理逻辑:

  1. 非容器节点直接返回
  2. 对象节点:检查字段名匹配且值为文本时替换,否则递归处理子节点
  3. 数组节点:递归处理每个元素

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

配置步骤:

  1. 获取模拟服务器端口
  2. 配置脱敏过滤器(替换 ssn 字段为 *
  3. 配置路径重写过滤器(将 /scrub 映射到 /customer
  4. 构建指向模拟服务的路由

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 访问并修改后端服务响应体的完整方案。核心技巧是:

  1. 继承 AbstractGatewayFilterFactory 实现自定义过滤器
  2. 利用 ModifyResponseBodyGatewayFilterFactory 处理响应体转换
  3. 通过递归遍历实现复杂 JSON 结构的修改

这种模式在数据脱敏、API 兼容性维护等场景特别实用,简单粗暴地解决了响应体处理的常见需求。


原始标题:Processing the Response Body in Spring Cloud Gateway | Baeldung