
Spring Cloud Gateway网关的过滤器链与自定义过滤器开发实战
大家好,作为一名常年和微服务架构“打交道”的开发者,我深知网关在系统中的地位。它就像小区的门卫,所有的请求都要经过它的盘查、引导和加工。Spring Cloud Gateway(后文简称SCG)作为Spring官方推出的第二代网关,其基于WebFlux的非阻塞式设计和强大的过滤器(Filter)机制,让我在构建灵活、高效的API网关时得心应手。今天,我就结合自己的实战经验,和大家深入聊聊SCG的过滤器链,并手把手带你开发一个实用的自定义过滤器,过程中遇到的“坑”也会一并分享。
一、理解网关过滤器链:请求的“流水线”加工厂
在开始写代码之前,我们必须先理解SCG的核心处理模型:过滤器链。你可以把一次请求在网关中的旅程想象成一条工厂流水线。
当一个请求到达SCG时,它会根据路由配置(Route)找到匹配的路径。每个路由都关联着一个特定的过滤器链。这个链由两类过滤器组成:
- 全局过滤器(GlobalFilter):作用于所有路由,无需在配置中显式声明。比如全局鉴权、日志记录。
- 路由过滤器(GatewayFilter):需要绑定到特定路由上,只对该路由的请求生效。比如对某个服务接口添加请求头、限流。
更精妙的是,过滤器在链中的执行顺序还分为“前置”(pre)和“后置”(post)两个阶段,这对应着请求转发到下游服务之前和收到响应之后。SCG内置了丰富的过滤器,比如 `AddRequestHeader`、`PrefixPath`、`RequestRateLimiter` 等,开箱即用。
我的踩坑提示:过滤器链的执行顺序非常关键,尤其是当多个过滤器修改同一个东西(比如请求头)时。全局过滤器可以通过 `@Order` 注解或实现 `Ordered` 接口来排序,数字越小优先级越高。路由过滤器的执行顺序则由其在配置文件中声明的顺序决定。
二、实战:开发一个自定义全局鉴权过滤器
理论说得再多,不如动手写一个。假设我们需要一个简单的全局鉴权过滤器,检查请求头中是否包含有效的 `X-Auth-Token`。我们将创建一个全局过滤器。
首先,在你的网关服务中创建一个类,实现 `GlobalFilter` 和 `Ordered` 接口。
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.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
@Component
public class CustomAuthFilter implements GlobalFilter, Ordered {
// 简单的Token校验逻辑(实战中应从数据库或缓存校验)
private boolean isValidToken(String token) {
return token != null && token.startsWith("valid-");
}
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String token = request.getHeaders().getFirst("X-Auth-Token");
// 1. 检查Token是否存在且有效
if (token == null || !isValidToken(token)) {
// 认证失败,拦截请求
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
String body = "{"code": 401, "message": "Unauthorized: Invalid or missing token."}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
// 2. 认证通过,可以在请求中附加一些信息(可选)
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", "parsed-user-id-from-token") // 例如从JWT解析出的用户ID
.build();
// 3. 将修改后的请求传入过滤器链继续执行
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
@Override
public int getOrder() {
// 设置一个较高的优先级,保证在大多数核心处理之前执行
return -100;
}
}
代码解析与实战经验:
- 我们通过 `exchange.getRequest()` 获取当前请求。注意,这里是响应式编程的 `ServerHttpRequest`。
- 校验失败时,我们直接构造响应并返回 `response.writeWith(...)`,中断过滤器链。这是拦截请求的关键。
- 校验成功时,我们使用 `request.mutate()` 来创建一个请求的副本并添加新的请求头,然后通过 `exchange.mutate()` 创建新的交换对象,继续传递。**切记:`ServerHttpRequest` 是不可变的,任何修改都必须通过 `mutate()` 进行。** 这是我早期容易忽略的一点。
- `getOrder()` 返回-100,让这个过滤器尽早执行。
三、开发一个自定义路由过滤器工厂
全局过滤器影响所有路由,有时我们需要更细粒度的控制。比如,只想对 `/api/user/**` 路径的请求添加一个响应头,报告处理耗时。这时就需要自定义路由过滤器。
SCG要求路由过滤器必须以 `GatewayFilterFactory` 结尾,并继承 `AbstractGatewayFilterFactory`。
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeUnit;
@Component
public class ElapsedTimeGatewayFilterFactory extends AbstractGatewayFilterFactory {
private static final String ELAPSED_TIME_BEGIN = "elapsedTimeBegin";
private static final String KEY = "withParams";
public ElapsedTimeGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// Pre 处理:记录开始时间
exchange.getAttributes().put(ELAPSED_TIME_BEGIN, System.nanoTime());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> { // Post 处理:计算耗时并添加到响应头
Long startTime = exchange.getAttribute(ELAPSED_TIME_BEGIN);
if (startTime != null) {
long elapsedNano = System.nanoTime() - startTime;
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(elapsedNano);
String value = elapsedMs + "ms";
if (config.isWithParams()) {
value += " (params:" + exchange.getRequest().getQueryParams() + ")";
}
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("X-Response-Time", value);
// 这里可以打个日志
System.out.println(exchange.getRequest().getURI() + " -> elapsed time: " + value);
}
})
);
};
}
public static class Config {
private boolean withParams = false; // 配置项:是否在输出中包含请求参数
public boolean isWithParams() {
return withParams;
}
public void setWithParams(boolean withParams) {
this.withParams = withParams;
}
}
@Override
public String name() {
// 在配置文件中使用的名称,默认是类名去掉‘GatewayFilterFactory’
return "ElapsedTime";
}
}
现在,我们可以在 `application.yml` 中使用这个自定义过滤器了:
spring:
cloud:
gateway:
routes:
- id: user_service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- name: ElapsedTime # 使用自定义过滤器
args:
withParams: true # 传递配置参数
- AddResponseHeader=X-Custom-Header, ExtraValue
核心要点与踩坑提示:
- 过滤器逻辑写在 `apply` 方法返回的 `GatewayFilter` 匿名函数中。`chain.filter(exchange)` 之前是 `pre` 逻辑,之后用 `.then()` 连接的是 `post` 逻辑。这是实现前后置处理的标准模式。
- 利用 `exchange.getAttributes()` 在请求的整个生命周期内共享数据(如开始时间),这是一个非常实用的技巧。
- 配置类 `Config` 允许你从YAML文件动态传入参数,使过滤器更灵活。
- 大坑预警:在 `post` 逻辑(`Mono.fromRunnable`)中,`exchange.getResponse()` 是可用的,但**绝不能**再去读取 `exchange.getRequest().getBody()`,因为请求体是一个只能被消费一次的响应式数据流,此时早已被下游服务消费,再次读取会报错。需要操作请求体内容的逻辑,必须在 `pre` 阶段通过修改请求对象来完成。
四、总结与最佳实践
通过上面的实战,我们可以看到,Spring Cloud Gateway的过滤器机制既强大又灵活。总结一下关键点:
- 明确需求选类型:影响所有路由用 `GlobalFilter`,针对特定路由用 `GatewayFilterFactory`。
- 理清执行阶段:想清楚你的逻辑应该在转发前(pre)还是获取响应后(post)执行。
- 注意不可变对象:请求和响应对象不可变,修改务必使用 `mutate()` 方法。
- 谨慎操作数据流:请求体只能消费一次,在 `post` 阶段避免再次读取。
- 善用属性上下文:使用 `exchange.getAttributes()` 在过滤器间传递数据。
网关作为系统的入口,其稳定性和性能至关重要。自定义过滤器让我们能精准控制流量,实现鉴权、限流、监控、参数转换等各种横切关注点。希望这篇结合实战和踩坑经验的分享,能帮助你在下次开发SCG过滤器时更加游刃有余。动手试试吧,遇到问题多查文档和源码,理解其响应式编程模型,你会越来越喜欢这个强大的工具。

评论(0)