領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多在本文中,我們將探討如何為Spring Cloud Gateway編寫自定義擴充套件。在開始之前,讓我們先了解一下Spring Cloud Gateway的工作原理

我們的擴充套件將對請求體進行雜湊處理,並將該值新增為一個名為 X-Hash 的請求頭。這對應於上圖中的步驟 3。注意:由於我們正在讀取請求體,閘道器將受到記憶體限制。
首先,我們在 start.spring.io 建立一個帶 Gateway 依賴項的專案。在此示例中,我們將使用 Java 上的 Gradle 專案,帶有 JDK 17 和 Spring Boot 2.7.3。下載、解壓並在您喜歡的 IDE 中開啟該專案並執行它,以確保您已為本地開發做好準備。
接下來,我們建立 GatewayFilter Factory,它是一個作用域限定於特定路由的過濾器,允許我們以某種方式修改傳入的 HTTP 請求或傳出的 HTTP 響應。在我們的例子中,我們將透過新增額外的請求頭來修改傳入的 HTTP 請求。
package com.example.demo;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.List;
import org.bouncycastle.util.encoders.Hex;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR;
/**
* This filter hashes the request body, placing the value in the X-Hash header.
* Note: This causes the gateway to be memory constrained.
* Sample usage: RequestHashing=SHA-256
*/
@Component
public class RequestHashingGatewayFilterFactory extends
AbstractGatewayFilterFactory<RequestHashingGatewayFilterFactory.Config> {
private static final String HASH_ATTR = "hash";
private static final String HASH_HEADER = "X-Hash";
private final List<HttpMessageReader<?>> messageReaders =
HandlerStrategies.withDefaults().messageReaders();
public RequestHashingGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
MessageDigest digest = config.getMessageDigest();
return (exchange, chain) -> ServerWebExchangeUtils
.cacheRequestBodyAndRequest(exchange, (httpRequest) -> ServerRequest
.create(exchange.mutate().request(httpRequest).build(),
messageReaders)
.bodyToMono(String.class)
.doOnNext(requestPayload -> exchange
.getAttributes()
.put(HASH_ATTR, computeHash(digest, requestPayload)))
.then(Mono.defer(() -> {
ServerHttpRequest cachedRequest = exchange.getAttribute(
CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR);
Assert.notNull(cachedRequest,
"cache request shouldn't be null");
exchange.getAttributes()
.remove(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR);
String hash = exchange.getAttribute(HASH_ATTR);
cachedRequest = cachedRequest.mutate()
.header(HASH_HEADER, hash)
.build();
return chain.filter(exchange.mutate()
.request(cachedRequest)
.build());
})));
}
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("algorithm");
}
private String computeHash(MessageDigest messageDigest, String requestPayload) {
return Hex.toHexString(messageDigest.digest(requestPayload.getBytes()));
}
static class Config {
private MessageDigest messageDigest;
public MessageDigest getMessageDigest() {
return messageDigest;
}
public void setAlgorithm(String algorithm) throws NoSuchAlgorithmException {
messageDigest = MessageDigest.getInstance(algorithm);
}
}
}
讓我們更詳細地檢視程式碼
@Component 註解。Spring Cloud Gateway 需要能夠檢測到這個類才能使用它。或者,我們也可以使用 @Bean 定義一個例項。GatewayFilterFactory 作為字尾。在 application.yaml 中新增此過濾器時,我們不包含字尾,只包含 RequestHashing。這是 Spring Cloud Gateway 過濾器命名約定。AbstractGatewayFilterFactory,類似於所有其他 Spring Cloud Gateway 過濾器。我們還指定了一個用於配置過濾器的類,一個名為 Config 的巢狀靜態類有助於保持簡單。配置類允許我們設定要使用的雜湊演算法。apply 方法是所有工作發生的地方。在引數中,我們得到了配置類的例項,可以在其中訪問用於雜湊的 MessageDigest 例項。接下來,我們看到 (exchange, chain),這是一個返回的 GatewayFilter 介面類的 lambda 表示式。exchange 是 ServerWebExchange 的例項,它為 Gateway 過濾器提供了訪問 HTTP 請求和響應的能力。對於我們的情況,我們想要修改 HTTP 請求,這要求我們修改 exchange。ServerWebExchangeUtils,我們將請求作為屬性快取在 exchange 中。屬性提供了一種在過濾器鏈中共享特定請求資料的方法。我們還將儲存計算出的請求體雜湊值。shortcutFieldOrder 方法有助於將引數的數量和順序對映到過濾器。algorithm 字串與 Config 類中的 setter 匹配。為了測試程式碼,我們將使用 WireMock。將依賴項新增到您的 build.gradle 檔案中。
testImplementation 'com.github.tomakehurst:wiremock:2.27.2'
這裡我們有一個測試檢查請求頭的存在和值,另一個測試檢查在沒有請求體的情況下請求頭是否不存在。
package com.example.demo;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import org.bouncycastle.jcajce.provider.digest.SHA512;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.reactive.server.WebTestClient;
import static com.example.demo.RequestHashingGatewayFilterFactory.*;
import static com.example.demo.RequestHashingGatewayFilterFactoryTest.*;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringBootTest(
webEnvironment = RANDOM_PORT,
classes = RequestHashingFilterTestConfig.class)
@AutoConfigureWebTestClient
class RequestHashingGatewayFilterFactoryTest {
@TestConfiguration
static class RequestHashingFilterTestConfig {
@Autowired
RequestHashingGatewayFilterFactory requestHashingGatewayFilter;
@Bean(destroyMethod = "stop")
WireMockServer wireMockServer() {
WireMockConfiguration options = wireMockConfig().dynamicPort();
WireMockServer wireMock = new WireMockServer(options);
wireMock.start();
return wireMock;
}
@Bean
RouteLocator testRoutes(RouteLocatorBuilder builder, WireMockServer wireMock)
throws NoSuchAlgorithmException {
Config config = new Config();
config.setAlgorithm("SHA-512");
GatewayFilter gatewayFilter = requestHashingGatewayFilter.apply(config);
return builder
.routes()
.route(predicateSpec -> predicateSpec
.path("/post")
.filters(spec -> spec.filter(gatewayFilter))
.uri(wireMock.baseUrl()))
.build();
}
}
@Autowired
WebTestClient webTestClient;
@Autowired
WireMockServer wireMockServer;
@AfterEach
void afterEach() {
wireMockServer.resetAll();
}
@Test
void shouldAddHeaderWithComputedHash() {
MessageDigest messageDigest = new SHA512.Digest();
String body = "hello world";
String expectedHash = Hex.toHexString(messageDigest.digest(body.getBytes()));
wireMockServer.stubFor(WireMock.post("/post").willReturn(WireMock.ok()));
webTestClient.post().uri("/post")
.bodyValue(body)
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.OK);
wireMockServer.verify(postRequestedFor(urlEqualTo("/post"))
.withHeader("X-Hash", equalTo(expectedHash)));
}
@Test
void shouldNotAddHeaderIfNoBody() {
wireMockServer.stubFor(WireMock.post("/post").willReturn(WireMock.ok()));
webTestClient.post().uri("/post")
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.OK);
wireMockServer.verify(postRequestedFor(urlEqualTo("/post"))
.withoutHeader("X-Hash"));
}
}
要在我們的閘道器中使用過濾器,我們將 RequestHashing 過濾器新增到 application.yaml 中的路由中,使用 SHA-256 作為演算法。
spring:
cloud:
gateway:
routes:
- id: demo
uri: https://httpbin.org
predicates:
- Path=/post/**
filters:
- RequestHashing=SHA-256
我們使用 https://httpbin.org,因為它在返回的響應中顯示我們的請求頭。執行應用程式併發出 curl 請求以檢視結果。
$> curl --request POST 'https://:8080/post' \
--header 'Content-Type: application/json' \
--data-raw '{
"data": {
"hello": "world"
}
}'
{
...
"data": "{\n \"data\": {\n \"hello\": \"world\"\n }\n}",
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"Content-Length": "48",
"Content-Type": "application/json",
"Forwarded": "proto=http;host=\"localhost:8080\";for=\"[0:0:0:0:0:0:0:1]:55647\"",
"Host": "httpbin.org",
"User-Agent": "PostmanRuntime/7.29.0",
"X-Forwarded-Host": "localhost:8080",
"X-Hash": "1bd93d38735501b5aec7a822f8bc8136d9f1f71a30c2020511bdd5df379772b8"
},
...
}
總而言之,我們看到了如何為 Spring Cloud Gateway 編寫自定義擴充套件。我們的過濾器讀取請求體以生成一個雜湊值,我們將其新增為請求頭。我們還使用 WireMock 為過濾器編寫了測試,以檢查請求頭值。最後,我們運行了一個帶有該過濾器的閘道器以驗證結果。
如果您計劃在 Kubernetes 叢集上部署 Spring Cloud Gateway,請務必檢視 VMware Spring Cloud Gateway for Kubernetes。除了支援開源 Spring Cloud Gateway 過濾器和自定義過濾器(例如我們上面編寫的過濾器)之外,它還附帶了 更多內建過濾器,用於操縱您的請求和響應。Spring Cloud Gateway for Kubernetes 代表 API 開發團隊處理橫切關注點,例如:單點登入(SSO)、訪問控制、速率限制、彈性、安全性等等。