Spring Cloud Gateway:自定义Global Filter对请求/响应进行修改的实现

Spring Cloud Gateway:自定义Global Filter对请求/响应进行修改的实现

大家好,今天我们要深入探讨Spring Cloud Gateway中一个非常重要的概念:Global Filter。Global Filter允许我们集中式地处理所有经过Gateway的请求和响应,进行各种通用操作,例如身份验证、授权、日志记录、请求/响应修改等等。我们将重点关注如何自定义Global Filter来修改请求和响应,并提供实际的代码示例。

1. Global Filter 简介

在Spring Cloud Gateway中,Filter是构成路由的核心组件。它可以拦截请求并对其进行处理,然后在将请求转发到下游服务之前或之后执行一些操作。Global Filter与Route Filter的区别在于,Global Filter不需要在路由配置中显式声明,而是自动应用于所有路由。这使得Global Filter非常适合处理跨越多个路由的通用逻辑。

Spring Cloud Gateway提供了许多内置的Global Filter,例如RouteToRequestUrlFilter(将请求路由到目标URL)、NettyRoutingFilter(使用Netty客户端进行路由)等等。但更重要的是,我们可以自定义Global Filter来满足特定的业务需求。

2. 自定义Global Filter的步骤

自定义Global Filter需要实现GlobalFilterOrdered接口。GlobalFilter接口定义了Filter的核心逻辑,而Ordered接口用于指定Filter的执行顺序。

步骤如下:

  1. 创建Filter类并实现GlobalFilterOrdered接口。
  2. 实现filter方法:这是Filter的核心逻辑,它接收一个ServerWebExchange对象和一个GatewayFilterChain对象。ServerWebExchange对象包含了请求和响应的上下文信息,GatewayFilterChain对象用于将请求传递给下一个Filter。
  3. 实现getOrder方法:该方法返回一个整数,用于指定Filter的执行顺序。数值越小,优先级越高。
  4. 将Filter类注册为Spring Bean。

3. 修改请求示例:添加请求头

让我们创建一个Global Filter,它会在所有请求中添加一个名为X-Custom-Header的请求头,值为CustomValue

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AddRequestHeaderGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest().mutate()
                .header("X-Custom-Header", "CustomValue")
                .build();

        ServerWebExchange modifiedExchange = exchange.mutate()
                .request(request)
                .build();

        return chain.filter(modifiedExchange);
    }

    @Override
    public int getOrder() {
        return 1; // 设置优先级,数值越小优先级越高
    }
}

代码解释:

  • @Component注解将该类注册为Spring Bean。
  • filter方法首先获取ServerWebExchange对象中的原始请求。
  • 使用mutate()方法创建一个ServerHttpRequest.Builder对象,允许修改请求。
  • 使用header()方法添加X-Custom-Header请求头。
  • 使用build()方法构建新的ServerHttpRequest对象。
  • 创建一个新的ServerWebExchange对象,使用修改后的ServerHttpRequest对象替换原始请求。
  • 调用chain.filter(modifiedExchange)将修改后的请求传递给下一个Filter。
  • getOrder()方法返回1,表示该Filter的优先级。

运行结果:

当任何请求经过Gateway时,都会自动添加X-Custom-Header: CustomValue请求头。你可以通过下游服务接收到的请求头来验证这一点。

4. 修改请求示例:修改请求体

修改请求体稍微复杂一些,因为我们需要读取原始请求体,进行修改,然后将其重新写入请求。以下示例演示如何将请求体中的所有字母转换为大写。

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

@Component
public class ModifyRequestBodyGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        // 只处理POST/PUT请求,并且Content-Type为application/json
        if (!HttpMethod.POST.equals(request.getMethod()) && !HttpMethod.PUT.equals(request.getMethod())) {
            return chain.filter(exchange);
        }

        if (!request.getHeaders().getContentType().equals(MediaType.APPLICATION_JSON)) {
            return chain.filter(exchange);
        }

        // 读取请求体
        return request.getBody()
                .reduce(new StringBuilder(), (sb, dataBuffer) -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    sb.append(new String(bytes, StandardCharsets.UTF_8));
                    return sb;
                })
                .flatMap(body -> {
                    String modifiedBody = body.toString().toUpperCase();

                    // 将修改后的请求体写入请求
                    byte[] modifiedBytes = modifiedBody.getBytes(StandardCharsets.UTF_8);
                    DataBuffer modifiedDataBuffer = exchange.getResponse().bufferFactory().wrap(modifiedBytes);
                    Flux<DataBuffer> modifiedBodyFlux = Flux.just(modifiedDataBuffer);

                    // 创建一个ServerHttpRequestDecorator来包装原始请求
                    ServerHttpRequestDecorator requestDecorator = new ServerHttpRequestDecorator(request) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return modifiedBodyFlux;
                        }

                        @Override
                        public HttpHeaders getHeaders() {
                            HttpHeaders httpHeaders = new HttpHeaders();
                            httpHeaders.putAll(super.getHeaders());
                            // 重新设置Content-Length
                            httpHeaders.setContentLength(modifiedBytes.length);
                            return httpHeaders;
                        }
                    };

                    // 传递修改后的请求给下一个Filter
                    return chain.filter(exchange.mutate().request(requestDecorator).build());
                });
    }

    @Override
    public int getOrder() {
        return 2;
    }
}

代码解释:

  • 该Filter仅处理POST和PUT请求,并且Content-Type为application/json。
  • 使用request.getBody()方法读取请求体,并将其转换为字符串。
  • 将字符串转换为大写。
  • 将修改后的字符串转换为字节数组,并创建一个新的DataBuffer对象。
  • 创建一个ServerHttpRequestDecorator对象来包装原始请求。ServerHttpRequestDecorator允许我们覆盖getBody()getHeaders()方法,从而修改请求体和请求头。
  • getBody()方法中,返回修改后的DataBuffer对象。
  • getHeaders()方法中,重新设置Content-Length请求头,以反映修改后的请求体大小。
  • 使用exchange.mutate().request(requestDecorator).build()创建一个新的ServerWebExchange对象,并将修改后的请求传递给下一个Filter。

注意:

  • 修改请求体是一个复杂的操作,需要小心处理。确保正确处理编码问题和Content-Length请求头。
  • ServerHttpRequestDecorator是一个非常有用的类,它允许我们以非侵入的方式修改请求。

5. 修改响应示例:添加响应头

类似于修改请求头,我们可以创建一个Global Filter来添加响应头。

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AddResponseHeaderGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            response.getHeaders().add("X-Custom-Response-Header", "ResponseValue");
        }));
    }

    @Override
    public int getOrder() {
        return 3;
    }
}

代码解释:

  • 该Filter使用chain.filter(exchange).then(Mono.fromRunnable(() -> ...))在请求被下游服务处理后执行。
  • then()方法中,我们获取ServerWebExchange对象中的响应。
  • 使用response.getHeaders().add()方法添加X-Custom-Response-Header响应头。

运行结果:

所有响应都会自动添加X-Custom-Response-Header: ResponseValue响应头。

6. 修改响应示例:修改响应体

修改响应体与修改请求体类似,也需要读取原始响应体,进行修改,然后将其重新写入响应。

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

@Component
public class ModifyResponseBodyGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = response.bufferFactory();

        // 只有Content-Type为application/json的响应才会被修改
        if (!response.getHeaders().getContentType().equals(MediaType.APPLICATION_JSON)) {
            return chain.filter(exchange);
        }

        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Flux<DataBuffer> body) {
                return super.writeWith(body.map(dataBuffer -> {
                    // 读取响应体
                    byte[] content = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(content);
                    String originalResponse = new String(content, StandardCharsets.UTF_8);

                    // 修改响应体(例如,将所有字母转换为小写)
                    String modifiedResponse = originalResponse.toLowerCase();

                    // 将修改后的响应体写入响应
                    byte[] modifiedContent = modifiedResponse.getBytes(StandardCharsets.UTF_8);
                    DataBuffer modifiedDataBuffer = bufferFactory.wrap(modifiedContent);

                    // 释放原始DataBuffer
                    //DataBufferUtils.release(dataBuffer); // 确保释放资源

                    return modifiedDataBuffer;
                }));
            }

            @Override
            public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
                return writeWith(Flux.from(body).flatMapSequential(publisher -> publisher));
            }
        };

        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }

    @Override
    public int getOrder() {
        return 4;
    }
}

代码解释:

  • 该Filter仅处理Content-Type为application/json的响应。
  • 创建一个ServerHttpResponseDecorator对象来包装原始响应。ServerHttpResponseDecorator允许我们覆盖writeWith()方法,从而修改响应体。
  • writeWith()方法中,我们读取响应体,将其转换为字符串,然后将字符串转换为小写。
  • 将修改后的字符串转换为字节数组,并创建一个新的DataBuffer对象。
  • 返回包含修改后的DataBuffer对象的Mono<Void>对象。

重要提示:

  • 修改响应体也会影响性能,因为需要读取和写入整个响应体。
  • 确保正确处理编码问题。
  • DataBufferUtils.release(dataBuffer); 被注释掉的原因是,在某些情况下,Spring Cloud Gateway会自动处理 DataBuffer 的释放。显式释放可能会导致 double-free 的问题。通常情况下,不需要手动释放 DataBuffer,除非你确定 Gateway 没有正确处理。 如果你发现内存泄漏,才考虑手动释放,并仔细测试。
  • 需要处理 writeAndFlushWith, 避免响应体没有正确被写入.

7. Filter的执行顺序

Ordered接口允许我们控制Global Filter的执行顺序。getOrder()方法返回一个整数,数值越小,优先级越高。这意味着getOrder()返回-1的Filter将在getOrder()返回0的Filter之前执行。

以下是一些建议的Filter执行顺序:

顺序 Filter 功能
-1 认证和授权
1 请求头修改
2 请求体修改
3 响应头修改
4 响应体修改
100 日志记录和监控

选择合适的Filter执行顺序非常重要,以确保Filter能够正确地执行其功能。例如,认证和授权Filter应该在请求头和请求体修改Filter之前执行,以确保请求的安全性。

8. 总结

我们学习了如何自定义Global Filter来修改Spring Cloud Gateway中的请求和响应。通过自定义Global Filter,我们可以集中式地处理各种通用操作,例如身份验证、授权、日志记录、请求/响应修改等等。我们通过添加请求头、修改请求体、添加响应头和修改响应体的示例,演示了如何使用Global Filter来修改请求和响应。最后,我们讨论了Filter的执行顺序,并提供了一些建议。

Global Filter 是 Spring Cloud Gateway 中非常强大且灵活的工具。掌握 Global Filter 的使用方法,可以极大地提高 Gateway 的可定制性和可扩展性。通过巧妙地组合和配置 Global Filter,可以构建出满足各种需求的 Gateway 解决方案。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注