知識庫 / Spring / Spring Cloud RSS 訂閱

Spring Cloud Gateway響應體處理

Spring Cloud
HongKong
6
12:00 PM · Dec 06 ,2025

1. 引言

在本教程中,我們將探討如何使用 Spring Cloud Gateway 來在將響應體發送回客户端之前,對其進行檢查和/或修改。

2. Spring Cloud Gateway 快速回顧

Spring Cloud Gateway(簡稱SCG)是 Spring Cloud 家族的一個子項目,它基於響應式 Web 堆棧構建的 API 網關。我們之前在早期教程中已經涵蓋了它的基本用法,這裏將不會深入探討這些方面。

相反,這次我們將重點關注在圍繞 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 會為使用我們過濾器的每個路由定義調用此方法。 例如,在上面的配置中,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()` 方法。

此方法接受三個參數:兩個類(in 和 out)和一個可以從傳入類型轉換為傳出類型的函數。 在我們的情況下,我們不進行類型轉換,因此輸入和輸出都使用相同的類: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,這使我們能夠訪問請求處理的上下文。我們這裏不會使用它,但知道我們擁有這種能力是有益的。下一個參數是接收到的 body,已經轉換為 informed 類。

預期的返回值是一個 Publisher,包含 informed 類實例。只要我們不進行任何阻塞 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 測試,用作清洗器(scrubber)的初步驗證。後者則更具價值,因為它展示了在 SCG 開發中使用的實用技術。

首先,需要提供一個實際的後端,以便可以發送消息。一種可能的方法是使用像 Postman 這樣的外部工具,但這可能會對典型的 CI/CD 場景產生問題。相反,我們將使用 JDK 中不太為人所知 <em >HttpServer</em> 類,該類實現了簡單的 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 庫對其進行修改。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.