创建自定义 Spring Cloud Gateway 过滤器 - spring.io

在本文中,我们着眼于为 Spring Cloud Gateway 编写自定义扩展。在开始之前,让我们回顾一下 Spring Cloud Gateway 的工作原理:

image.png
  1. 首先,客户端向网关发出网络请求
  2. 网关定义了许多路由,每个路由都有谓词来匹配请求和路由。例如,您可以匹配 URL 的路径段或请求的 HTTP 方法。
  3. 一旦匹配,网关对应用于路由的每个过滤器执行预请求逻辑。例如,您可能希望将查询参数添加到您的请求中
  4. 代理过滤器将请求路由到代理服务
  5. 服务执行并返回响应
  6. 网关接收响应并在返回响应之前对每个过滤器执行请求后逻辑。例如,您可以在返回客户端之前删除不需要的响应标头。

我们的扩展将对请求正文进行哈希处理,并将该值添加为名为 的请求标头X-Hash。这对应于上图中的步骤 3。注意:当我们读取请求正文时,网关将受到内存限制。

首先,我们在 start.spring.io 中创建一个具有 Gateway 依赖项的项目。在此示例中,我们将在 Java 中使用带有 JDK 17 和 Spring Boot 2.7.3 的 Gradle 项目。下载、解压缩并在您喜欢的 IDE 中打开项目并运行它,以确保您已为本地开发做好准备。

接下来让我们创建 GatewayFilter Factory,它是一个限定于特定路由的过滤器,它允许我们以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应。在我们的例子中,我们将使用附加标头修改传入的 HTTP 请求:

<pre class="prettyprint hljs erb" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; word-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"><b>package</b> com.example.demo;

<b>import</b> java.security.MessageDigest;
<b>import</b> java.security.NoSuchAlgorithmException;
<b>import</b> java.util.Collections;
<b>import</b> java.util.List;

<b>import</b> org.bouncycastle.util.encoders.Hex;
<b>import</b> reactor.core.publisher.Mono;

<b>import</b> org.springframework.cloud.gateway.filter.GatewayFilter;
<b>import</b> org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
<b>import</b> org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
<b>import</b> org.springframework.http.codec.HttpMessageReader;
<b>import</b> org.springframework.http.server.reactive.ServerHttpRequest;
<b>import</b> org.springframework.stereotype.Component;
<b>import</b> org.springframework.util.Assert;
<b>import</b> org.springframework.web.reactive.function.server.HandlerStrategies;
<b>import</b> org.springframework.web.reactive.function.server.ServerRequest;

<b>import</b> <b>static</b> org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR;

<font><i>/**

  • 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
    */</i></font><font>
    @Component
    <b>public</b> <b>class</b> RequestHashingGatewayFilterFactory <b>extends</b>
    AbstractGatewayFilterFactory<RequestHashingGatewayFilterFactory.Config> {

    <b>private</b> <b>static</b> <b>final</b> String HASH_ATTR = </font><font>"hash"</font><font>;
    <b>private</b> <b>static</b> <b>final</b> String HASH_HEADER = </font><font>"X-Hash"</font><font>;
    <b>private</b> <b>final</b> List<HttpMessageReader<?>> messageReaders =
    HandlerStrategies.withDefaults().messageReaders();

    <b>public</b> RequestHashingGatewayFilterFactory() {
    <b>super</b>(Config.<b>class</b>);
    }

    @Override
    <b>public</b> GatewayFilter apply(Config config) {
    MessageDigest digest = config.getMessageDigest();
    <b>return</b> (exchange, chain) -> ServerWebExchangeUtils
    .cacheRequestBodyAndRequest(exchange, (httpRequest) -> ServerRequest
    .create(exchange.mutate().request(httpRequest).build(),
    messageReaders)
    .bodyToMono(String.<b>class</b>)
    .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,
    </font><font>"cache request shouldn't be null"</font><font>);
    exchange.getAttributes()
    .remove(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR);

                     String hash = exchange.getAttribute(HASH_ATTR);
                     cachedRequest = cachedRequest.mutate()
                             .header(HASH_HEADER, hash)
                             .build();
                     <b>return</b> chain.filter(exchange.mutate()
                             .request(cachedRequest)
                             .build());
                 })));
    

    }

    @Override
    <b>public</b> List<String> shortcutFieldOrder() {
    <b>return</b> Collections.singletonList(</font><font>"algorithm"</font><font>);
    }

    <b>private</b> String computeHash(MessageDigest messageDigest, String requestPayload) {
    <b>return</b> Hex.toHexString(messageDigest.digest(requestPayload.getBytes()));
    }

    <b>static</b> <b>class</b> Config {

     <b>private</b> MessageDigest messageDigest;
    
     <b>public</b> MessageDigest getMessageDigest() {
         <b>return</b> messageDigest;
     }
    
     <b>public</b> <b>void</b> setAlgorithm(String algorithm) throws NoSuchAlgorithmException {
         messageDigest = MessageDigest.getInstance(algorithm);
     }
    

    }
    }
    </font></pre>

让我们更详细地看一下代码:

  • 我们为该类添加了@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请求,这就要求我们对交换进行变异。
  • 我们需要读取请求体来产生哈希值,然而,由于请求体被存储在一个字节缓冲区中,它在过滤器中只能被读取一次。通过使用ServerWebExchangeUtils,我们把请求作为交换中的一个属性进行缓存。属性提供了一种在过滤器链中共享特定请求数据的方式。我们也将存储请求主体的计算哈希值。
  • 我们使用交换的属性来获取缓存的请求和计算的哈希值。然后我们通过添加哈希头来突变交换,最后将其发送到链上的下一个过滤器。
  • shortcutFieldOrder方法有助于将参数的数量和顺序映射到过滤器中。该算法字符串与配置类中的setter相匹配。

为了测试代码,我们将使用 WireMock。将依赖项添加到您的build.gradle文件中:

<pre class="hljs nginx" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 0.75em; font-size: 14px; line-height: 1.5em; word-break: break-all; word-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">testImplementation 'com.github.tomakehurst:wiremock:2.27.2'</pre>

在这里,我们有一个测试检查头的存在和价值,如果没有请求正 ,文另一个测试检查头是否不存在。

<pre class="prettyprint hljs xml" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; word-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"><b>package</b> com.example.demo;

<b>import</b> java.security.MessageDigest;
<b>import</b> java.security.NoSuchAlgorithmException;

<b>import</b> com.github.tomakehurst.wiremock.WireMockServer;
<b>import</b> com.github.tomakehurst.wiremock.client.WireMock;
<b>import</b> com.github.tomakehurst.wiremock.core.WireMockConfiguration;
<b>import</b> org.bouncycastle.jcajce.provider.digest.SHA512;
<b>import</b> org.bouncycastle.util.encoders.Hex;
<b>import</b> org.junit.jupiter.api.AfterEach;
<b>import</b> org.junit.jupiter.api.Test;

<b>import</b> org.springframework.beans.factory.annotation.Autowired;
<b>import</b> org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
<b>import</b> org.springframework.boot.test.context.SpringBootTest;
<b>import</b> org.springframework.boot.test.context.TestConfiguration;
<b>import</b> org.springframework.cloud.gateway.filter.GatewayFilter;
<b>import</b> org.springframework.cloud.gateway.route.RouteLocator;
<b>import</b> org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
<b>import</b> org.springframework.context.annotation.Bean;
<b>import</b> org.springframework.http.HttpStatus;
<b>import</b> org.springframework.test.web.reactive.server.WebTestClient;

<b>import</b> <b>static</b> com.example.demo.RequestHashingGatewayFilterFactory.;
<b>import</b> <b>static</b> com.example.demo.RequestHashingGatewayFilterFactoryTest.
;
<b>import</b> <b>static</b> com.github.tomakehurst.wiremock.client.WireMock.equalTo;
<b>import</b> <b>static</b> com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
<b>import</b> <b>static</b> com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
<b>import</b> <b>static</b> com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
<b>import</b> <b>static</b> org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

@SpringBootTest(
webEnvironment = RANDOM_PORT,
<b>class</b>es = RequestHashingFilterTestConfig.<b>class</b>)
@AutoConfigureWebTestClient
<b>class</b> RequestHashingGatewayFilterFactoryTest {

@TestConfiguration
<b>static</b> <b>class</b> RequestHashingFilterTestConfig {

    @Autowired
    RequestHashingGatewayFilterFactory requestHashingGatewayFilter;

    @Bean(destroyMethod = <font>"stop"</font><font>)
    WireMockServer wireMockServer() {
        WireMockConfiguration options = wireMockConfig().dynamicPort();
        WireMockServer wireMock = <b>new</b> WireMockServer(options);
        wireMock.start();
        <b>return</b> wireMock;
    }

    @Bean
    RouteLocator testRoutes(RouteLocatorBuilder builder, WireMockServer wireMock)
            throws NoSuchAlgorithmException {
        Config config = <b>new</b> Config();
        config.setAlgorithm(</font><font>"SHA-512"</font><font>);

        GatewayFilter gatewayFilter = requestHashingGatewayFilter.apply(config);
        <b>return</b> builder
                .routes()
                .route(predicateSpec -> predicateSpec
                        .path(</font><font>"/post"</font><font>)
                        .filters(spec -> spec.filter(gatewayFilter))
                        .uri(wireMock.baseUrl()))
                .build();
    }
}

@Autowired
WebTestClient webTestClient;

@Autowired
WireMockServer wireMockServer;

@AfterEach
<b>void</b> afterEach() {
    wireMockServer.resetAll();
}

@Test
<b>void</b> shouldAddHeaderWithComputedHash() {
    MessageDigest messageDigest = <b>new</b> SHA512.Digest();
    String body = </font><font>"hello world"</font><font>;
    String expectedHash = Hex.toHexString(messageDigest.digest(body.getBytes()));

    wireMockServer.stubFor(WireMock.post(</font><font>"/post"</font><font>).willReturn(WireMock.ok()));

    webTestClient.post().uri(</font><font>"/post"</font><font>)
            .bodyValue(body)
            .exchange()
            .expectStatus()
            .isEqualTo(HttpStatus.OK);

    wireMockServer.verify(postRequestedFor(urlEqualTo(</font><font>"/post"</font><font>))
            .withHeader(</font><font>"X-Hash"</font><font>, equalTo(expectedHash)));
}

@Test
<b>void</b> shouldNotAddHeaderIfNoBody() {
    wireMockServer.stubFor(WireMock.post(</font><font>"/post"</font><font>).willReturn(WireMock.ok()));

    webTestClient.post().uri(</font><font>"/post"</font><font>)
            .exchange()
            .expectStatus()
            .isEqualTo(HttpStatus.OK);

    wireMockServer.verify(postRequestedFor(urlEqualTo(</font><font>"/post"</font><font>))
            .withoutHeader(</font><font>"X-Hash"</font><font>));
}

}
</font></pre>

为了在我们的网关中使用该过滤器,我们在application.yaml的路由中添加RequestHashing过滤器,使用SHA-256作为算法。

<pre class="prettyprint hljs javascript" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; word-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">spring:
cloud:
gateway:
routes:
- id: demo
uri: https:<font><i>//httpbin.org</i></font><font>
predicates:
- Path=/post</font><font><i>/**
filters:
- RequestHashing=SHA-256
</i></font></pre>

我们使用https://httpbin.org,因为它在其返回的响应中显示了我们的请求头信息。运行应用程序,并进行curl请求以查看结果。

<pre class="prettyprint hljs xquery" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; word-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">$> curl --request POST 'http://localhost:8080/post'
--header 'Content-Type: application/json'
--data-raw '{
"data": {
"hello": "world"
}
}'

{
...
"data": "{\n &#34data": {\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"
},
...
}</pre>

综上所述,我们看到了如何为Spring Cloud Gateway编写一个自定义扩展。我们的过滤器读取了请求的主体,产生了一个哈希值,我们将其作为请求头添加。我们还使用WireMock为该过滤器编写了测试,以检查头的值。最后,我们用该过滤器运行了一个网关来验证结果。

如果你打算在Kubernetes集群上部署Spring Cloud Gateway,一定要查看VMware Spring Cloud Gateway for Kubernetes。除了支持开源的Spring Cloud Gateway过滤器和自定义过滤器(比如我们上面写的那个),它还配有更多的内置过滤器来处理你的请求和响应。Spring Cloud Gateway for Kubernetes代表API开发团队处理跨领域的问题,例如。单点登录(SSO)、访问控制、速率限制、弹性、安全等等。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容