网关不是“转发一下”那么简单
Spring Cloud Gateway 在微服务里承担的是入口职责:按路径把请求分到不同服务,做鉴权、限流、日志、灰度,必要时还要改写请求和响应。很多项目接上网关后只写了一两条 Path 路由,等到要做统一鉴权、跨域、traceId 透传时,才发现过滤器的执行顺序和请求体的可读性都不是想当然的。
这篇基于 Spring Cloud Gateway 4.x(Spring Boot 3.3),从路由结构讲到自定义过滤器,最后集中说几个高频坑。
路由三要素:id、predicate、filter
一条路由由三部分组成:唯一 id、匹配条件 predicate、处理链 filter。uri 用 lb://服务名 表示走注册中心负载均衡,用 http:// 表示直连:
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://order-service
order: 10
predicates:
- Path=/api/order/**
- Method=GET,POST
filters:
- StripPrefix=1
- AddRequestHeader=X-Gateway, order
- name: CircuitBreaker
args:
name: orderCircuitBreaker
fallbackUri: forward:/fallback/order
StripPrefix=1 会去掉路径的第一段:/api/order/1 变成 /order/1。数字数错是新手最常见的 404 来源,建议先在纸上把“网关收到的路径”和“服务实际暴露的路径”各写一遍。
order 决定同一请求命中多条路由时谁优先,值越小越先匹配。依赖 YAML 书写顺序并不可靠,跨配置文件合并后顺序更不可控,显式写 order 最省事。
过滤器顺序:pre 正序,post 逆序
Gateway 的过滤器分两类:绑定在某条路由上的 GatewayFilter,以及作用于所有路由的 GlobalFilter。它们按 Ordered 排序后组成一条链,请求进来时 order 小的先执行 pre 逻辑,响应返回时反过来,order 大的先执行 post 逻辑。
框架内置过滤器的顺序大致是:RemoveCachedBodyFilter、AdaptCachedBodyGlobalFilter 这类处理请求体的在最前面,NettyRoutingFilter 接近最后真正发请求,NettyWriteResponseFilter 的 order 是 -1,负责把下游响应写回客户端。理解这一点很重要:如果你的自定义过滤器想在响应写回之后做事,仅靠“业务上最后执行”是做不到的,必须把 order 设得比 -1 更小。
常见的顺序错误是鉴权过滤器 order 设得太大,等限流、日志都执行完才校验身份;或者 traceId 过滤器在下游调用之后才加头,导致下游拿不到。
自定义全局过滤器
下面这个过滤器做两件事:补齐 traceId 并透传给下游,同时把 traceId 写回响应头,方便前端和网关日志关联:
@Component
public class TraceIdGlobalFilter implements GlobalFilter, Ordered {
private static final String TRACE_ID = "X-Trace-Id";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String traceId = exchange.getRequest().getHeaders().getFirst(TRACE_ID);
if (!StringUtils.hasText(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
ServerHttpRequest request = exchange.getRequest().mutate()
.header(TRACE_ID, traceId)
.build();
exchange.getResponse().getHeaders().set(TRACE_ID, traceId);
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 10;
}
}
注意 exchange.getRequest().mutate() 返回的是新请求对象,必须通过 exchange.mutate().request(...) 放回去,直接改原对象是不生效的。另外不要在这个过滤器里 block() 或读请求体,WebFlux 的线程不能被阻塞。
常见坑与排查
坑一:和 Spring MVC 混用。 网关依赖是 WebFlux 栈,类路径上只要有 spring-boot-starter-web,启动就会报 Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway。网关模块不要引 web starter,需要 WebMVC 能力就用 spring-cloud-starter-gateway-mvc,但两者只能选一个。
坑二:lb:// 找不到实例。 路由匹配成功但返回 503,通常是服务名拼错、注册中心 namespace/group 与网关不一致,或者漏了 spring-cloud-starter-loadbalancer。先看 /actuator/gateway/routes 里路由是否存在,再查注册中心。
坑三:过滤器里读了请求体,下游收到空 body。 WebFlux 的请求体是只能消费一次的 Flux<DataBuffer>。要读就得先缓存,用 ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR 或 ModifyRequestBody 过滤器,并且注意 spring.codec.max-in-memory-size 的限制,别把大文件请求整个读进内存。
坑四:跨域头重复。 网关和下游都配了 CORS,响应里会出现两个 Access-Control-Allow-Origin,浏览器直接拒绝。要么只在网关配,要么加 DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST。
排查顺序:先打开 logging.level.org.springframework.cloud.gateway=DEBUG,看请求命中了哪条路由、经过哪些过滤器;再查 /actuator/gateway/routes;然后跳过网关直连服务,确认下游本身是否正常;最后看响应头和状态码,区分是网关拒绝、下游报错还是超时。多数“网关 404”不是网关的问题,而是 Path 前缀和 StripPrefix 没算对。
评论