領先一步
VMware 提供培訓和認證,以加速您的進展。
了解更多在本文中,我們將探討如何為 Spring Cloud Gateway 撰寫自訂擴充功能。在開始之前,讓我們先了解 Spring Cloud Gateway 的運作方式
我們的擴充功能將會雜湊請求 body,並將該值新增為名為 X-Hash
的請求標頭。這對應於上圖中的步驟 3。注意:由於我們正在讀取請求 body,閘道的記憶體將會受到限制。
首先,我們在 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 中的屬性。屬性提供了一種跨篩選器鏈為特定請求共用資料的方式。我們還將儲存請求 body 的計算雜湊值。shortcutFieldOrder
方法有助於將引數的數量和順序對應到篩選器。algorithm
字串與 Config
類別中的 setter 相符。為了測試程式碼,我們將使用 WireMock。將相依性新增至您的 build.gradle
檔案
testImplementation 'com.github.tomakehurst:wiremock:2.27.2'
這裡我們有一個測試檢查標頭的存在和值,另一個測試檢查如果沒有請求 body,則標頭不存在
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://127.0.0.1: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 撰寫自訂擴充功能。我們的篩選器讀取請求的 body 以產生雜湊值,我們將其新增為請求標頭。我們還使用 WireMock 為篩選器編寫了測試,以檢查標頭值。最後,我們執行了一個包含篩選器的閘道,以驗證結果。
如果您計劃在 Kubernetes 叢集上部署 Spring Cloud Gateway,請務必查看 VMware Spring Cloud Gateway for Kubernetes。除了支援開放原始碼 Spring Cloud Gateway 篩選器和自訂篩選器(例如我們上面撰寫的篩選器)之外,它還附帶 更多內建篩選器 來操作您的請求和回應。Spring Cloud Gateway for Kubernetes 代表 API 開發團隊處理跨領域問題,例如:單一登入 (SSO)、存取控制、速率限制、彈性、安全性等等。