Spring Cloud Gateway - 修改全局后置过滤器中的响应正文

2024-01-02

在过去的两天里,我在请求到达客户端之前尝试了各种可能的方法来修改请求的响应正文,但似乎没有任何方法对我有用。到目前为止我已经尝试过提到的实现here https://gist.github.com/dalegaspi/03550807b1c84100dd028fc8e07950ff, here https://stackoverflow.com/questions/55574382/spring-cloud-gateway-for-composite-api-calls/71888244#71888244, here https://stackoverflow.com/questions/50101750/how-to-modify-the-response-body-in-spring-cloud-gateway-just-before-the-commit, here https://stackoverflow.com/questions/71465870/how-to-get-original-response-body-in-spring-cloud-gateway-webflux-post-filter, here https://gist.github.com/WeirdBob/b25569d461f0f54444d2c0eab51f3c48还有其他一些我现在找不到的,但没有任何效果。我是否将过滤器定义为前置、后置、全局、网关或特定于路由并不重要 - 实际的响应修改似乎对我不起作用。

我的情况如下: 我正在运行一个配置了 YAML 的 API 网关,并配置了其路由之一以在后台引导至 ADF 服务。我对该 ADF 应用程序遇到的问题是,它返回给客户端的响应采用由其后端自动生成的 HTML 模板的形式。在此模板中,某些 URL 是硬编码的并指向应用程序本身的地址。为了证明在这种情况下使用 API 网关的合理性,我想将这些 ADF URL 替换为 API 网关的 URL。

为简单起见,假设我的 ADF 服务的 IP 地址是1.2.3.4:1234,我的API网关的IP地址是localhost:8080。当我在网关中点击 ADF 路由时,响应包含一些自动生成的 JavaScript 插入内容,例如:

AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://1.2.3.4:1234/entry/dynamic/index.jspx");

正如您所看到的,它包含一个硬编码的 URL。我想访问响应正文并找到所有这些硬编码的 URL 并将它们替换为网关 URL,因此上面的示例变为:

AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://localhost:8080/entry/dynamic/index.jspx");

为此,我认为明智的做法是拥有一个全局 POST 过滤器,仅当请求与我的 ADF 应用程序的路由匹配时才会启动,所以这就是我决定要做的事情。

这是到目前为止我的后置过滤器:

        @Bean
    public GlobalFilter globalADFUrlReplacementFilter() {
        return (exchange, chain) -> chain.filter(exchange).then(Mono.just(exchange)).map(serverWebExchange -> {
                    ServerHttpRequest request = exchange.getRequest();
                    ServerHttpResponse response = exchange.getResponse();

                    if (requestIsTowardsADF(request)) {
                        logger.info("EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT");
                        ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {

                            @Override
                            @SuppressWarnings("unchecked")
                            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                                logger.info("OVERRIDING writeWith METHOD TO MODIFY THE BODY");
                                Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
                                return super.writeWith(flux.buffer().map(buffer -> {

                                    DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                                    DataBuffer join = dataBufferFactory.join(buffer);
                                    byte[] content = new byte[join.readableByteCount()];
                                    join.read(content);
                                    DataBufferUtils.release(join);

                                    String bodyStr = new String(content, StandardCharsets.UTF_8);
                                    bodyStr = bodyStr.replace(ADF_URL, API_GATEWAY_URL);

                                    getDelegate().getHeaders().setContentLength(bodyStr.getBytes().length);
                                    return bufferFactory().wrap(bodyStr.getBytes());
                                }));
                            }
                        };
                        logger.info("ADF URL REPLACEMENT FILTER DONE");
                        return chain.filter(serverWebExchange.mutate().request(request).response(responseDecorator).build());
                    }
                    return serverWebExchange;
                })
                .then();
    }

和配置:

spring:
  cloud:
    gateway:
      routes:
        - id: adf-test-2
          uri: http://1.2.3.4:1234
          predicates:
            - Path=/entry/**

你可以看到我正在使用org.slf4j.Logger对象在控制台中记录消息。当我运行 API Gateway 并点击 ADF 路由时,我可以看到以下内容:

EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT
ADF URL REPLACEMENT FILTER DONE

当我检查从 API 网关返回的响应时,我可以看到响应正文仍然相同,并且 ADF URL 根本没有被替换。我尝试调试应用程序,一旦它到达ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {它会跳过这些花括号内的整个匿名类实现。证明这一点的是缺乏OVERRIDING writeWith METHOD TO MODIFY THE BODY登录控制台 - 它从未被执行!

似乎由于某种原因,实际的身体修改没有被执行,我不明白为什么。我尝试了此过滤器的几种不同实现,如上面的链接中所述,但它们都不起作用。

有人可以与我分享一个可以修改响应正文的工作 POST 过滤器,或者指出我的解决方案中的缺陷吗?

提前谢谢大家!


感谢您分享此示例过滤器 cdan。我使用它作为模板为我的问题提供了最直接的解决方案。它看起来是这样的:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

@Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {

    public static final String ADF_URL = "1.2.3.4:1234";
    public static final String AG_URL = "localhost:8080";
    final Logger logger = LoggerFactory.getLogger(TestFilter2.class);

    public static class Config {
        private String param1;

        public Config() {
        }

        public void setParam1(String param1) {
            this.param1 = param1;
        }

        public String getParam1() {
            return param1;
        }
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("param1");
    }

    private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;

    public TestFilter2() {
        super(Config.class);
        this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
    }

    @Override
    public GatewayFilter apply(Config config) {
        final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();

        modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));

        return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
    }

}

我已将此过滤器添加到我的路线定义中,如下所示:

spring:
  cloud:
    gateway:
      httpclient:
        wiretap: true
      httpserver:
        wiretap: true
      routes:
        - id: adf-test-2
          uri: http://1.2.3.4:1234
          predicates:
            - Path=/entry/**
          filters:
            - TestFilter2

我只是尝试修改响应正文并将其中的 ADF URL 替换为 AG URL,但每当我尝试访问 ADF 路由时,都会收到以下异常:

2022-05-08 17:35:19.492 ERROR 87216 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [284b180d-1]  500 Server Error for HTTP GET "/entry/dynamic/index.jspx"

org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String
    at org.springframework.web.reactive.function.BodyExtractors.lambda$readWithMessageReaders$12(BodyExtractors.java:201) ~[spring-webflux-5.3.18.jar:5.3.18]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ? Body from UNKNOWN  [DefaultClientResponse]
    *__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
    *__checkpoint ? HTTP GET "/entry/dynamic/index.jspx" [ExceptionHandlingWebHandler]

我在网上搜索了一段时间,但无法找到任何明确的答案来解释为什么会这样UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String当我尝试使用时抛出异常bodyAsString应该包含字符串形式的响应正文的字段。调试整个过滤器也不起作用,因为异常似乎在我到达路由后立即抛出,而且我什至无法进入该类的主体。我错过了一些明显的东西吗?

更新(2022 年 5 月 9 日):进一步研究后,我通过删除配置中不必要的参数并自动装配对过滤器的依赖来重构过滤器结构ModifyResponseBodyGatewayFilterFactory,现在看来过滤器工作正常并且完成了我需要的更换。我将对其进行更长时间的测试,以确保它确实按预期工作,如果可以,我会将其标记为解决方案。感谢您对 cdan 的所有贡献!

这是整个过滤器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {

    public static final String ADF_URL = "1.2.3.4:1234";
    public static final String AG_URL = "localhost:8080";
    @Autowired
    private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;

    public static class Config {
        public Config() {
        }
    }

    public TestFilter2(ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory) {
        super(Config.class);
        this.modifyResponseBodyFilterFactory = modifyResponseBodyFilterFactory;
    }

    @Override
    public GatewayFilter apply(Config config) {
        final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
        modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
        return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring Cloud Gateway - 修改全局后置过滤器中的响应正文 的相关文章

随机推荐