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需要实现GlobalFilter和Ordered接口。GlobalFilter接口定义了Filter的核心逻辑,而Ordered接口用于指定Filter的执行顺序。
步骤如下:
- 创建Filter类并实现
GlobalFilter和Ordered接口。 - 实现
filter方法:这是Filter的核心逻辑,它接收一个ServerWebExchange对象和一个GatewayFilterChain对象。ServerWebExchange对象包含了请求和响应的上下文信息,GatewayFilterChain对象用于将请求传递给下一个Filter。 - 实现
getOrder方法:该方法返回一个整数,用于指定Filter的执行顺序。数值越小,优先级越高。 - 将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 解决方案。