概述
断言决定请求"走不走这条路由",过滤器决定"走过去的时候对请求和响应做哪些加工"。这一节把 GatewayFilter 的两个作用域(路由过滤器、默认过滤器)和常用过滤器工厂的 yaml 写法一次讲透,顺带说清它跟 GlobalFilter 到底差在哪,避免把两者混成一个东西。
纲要
- 过滤器在网关链路里的位置:断言 → 过滤器链 → 微服务 → 过滤器链 → 响应
- 两类作用域
- 路由过滤器(
routes[].filters):只对当前这条路由生效 - 默认过滤器(
spring.cloud.gateway.default-filters):对所有路由生效,但它不是 GlobalFilter
- 路由过滤器(
- 常用过滤器工厂
AddRequestHeader/AddRequestParameter/AddResponseHeaderStripPrefix/PrefixPath/RewritePathSetStatus
- 参数写法 :
- Name=参数,逗号分隔多参数,=与值之间不留空格 - 动手验证:给 user-service 加请求头回显接口,再用 curl 走网关观察
- 执行顺序:route filters 与 default-filters 的关系(讲义口径)
- 实战坑 :
StripPrefix=1数字算错、值里带逗号被截断、缩进写错、GatewayFilter 当成 GlobalFilter
过滤器在网关链路里的位置
先把这个组件放到链路里看。用户请求不能直连微服务,必须先过网关(本项目 gateway 端口 10010)。网关内部是两段工作:
- 路由断言(Predicate) :根据配置规则判断这条请求该交给哪个微服务。
- Path=/user/**命中 user-service,- Path=/order/**命中 order-service。 - 路由过滤器(GatewayFilter) :断言匹配成功之后,请求不是立刻发往下游,而是先穿过一条过滤器链,链上每个过滤器都能对请求做加工;下游返回响应后,响应同样要再穿过过滤器链才回到客户端。
#mermaid-svg-EgJcyjYseWwexE7i{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EgJcyjYseWwexE7i .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EgJcyjYseWwexE7i .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EgJcyjYseWwexE7i .error-icon{fill:#552222;}#mermaid-svg-EgJcyjYseWwexE7i .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EgJcyjYseWwexE7i .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EgJcyjYseWwexE7i .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EgJcyjYseWwexE7i .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EgJcyjYseWwexE7i .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EgJcyjYseWwexE7i .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EgJcyjYseWwexE7i .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EgJcyjYseWwexE7i .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EgJcyjYseWwexE7i .marker.cross{stroke:#333333;}#mermaid-svg-EgJcyjYseWwexE7i svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EgJcyjYseWwexE7i p{margin:0;}#mermaid-svg-EgJcyjYseWwexE7i .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EgJcyjYseWwexE7i .cluster-label text{fill:#333;}#mermaid-svg-EgJcyjYseWwexE7i .cluster-label span{color:#333;}#mermaid-svg-EgJcyjYseWwexE7i .cluster-label span p{background-color:transparent;}#mermaid-svg-EgJcyjYseWwexE7i .label text,#mermaid-svg-EgJcyjYseWwexE7i span{fill:#333;color:#333;}#mermaid-svg-EgJcyjYseWwexE7i .node rect,#mermaid-svg-EgJcyjYseWwexE7i .node circle,#mermaid-svg-EgJcyjYseWwexE7i .node ellipse,#mermaid-svg-EgJcyjYseWwexE7i .node polygon,#mermaid-svg-EgJcyjYseWwexE7i .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EgJcyjYseWwexE7i .rough-node .label text,#mermaid-svg-EgJcyjYseWwexE7i .node .label text,#mermaid-svg-EgJcyjYseWwexE7i .image-shape .label,#mermaid-svg-EgJcyjYseWwexE7i .icon-shape .label{text-anchor:middle;}#mermaid-svg-EgJcyjYseWwexE7i .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EgJcyjYseWwexE7i .rough-node .label,#mermaid-svg-EgJcyjYseWwexE7i .node .label,#mermaid-svg-EgJcyjYseWwexE7i .image-shape .label,#mermaid-svg-EgJcyjYseWwexE7i .icon-shape .label{text-align:center;}#mermaid-svg-EgJcyjYseWwexE7i .node.clickable{cursor:pointer;}#mermaid-svg-EgJcyjYseWwexE7i .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EgJcyjYseWwexE7i .arrowheadPath{fill:#333333;}#mermaid-svg-EgJcyjYseWwexE7i .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EgJcyjYseWwexE7i .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EgJcyjYseWwexE7i .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EgJcyjYseWwexE7i .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EgJcyjYseWwexE7i .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EgJcyjYseWwexE7i .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EgJcyjYseWwexE7i .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EgJcyjYseWwexE7i .cluster text{fill:#333;}#mermaid-svg-EgJcyjYseWwexE7i .cluster span{color:#333;}#mermaid-svg-EgJcyjYseWwexE7i div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EgJcyjYseWwexE7i .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EgJcyjYseWwexE7i rect.text{fill:none;stroke-width:0;}#mermaid-svg-EgJcyjYseWwexE7i .icon-shape,#mermaid-svg-EgJcyjYseWwexE7i .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EgJcyjYseWwexE7i .icon-shape p,#mermaid-svg-EgJcyjYseWwexE7i .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EgJcyjYseWwexE7i .icon-shape .label rect,#mermaid-svg-EgJcyjYseWwexE7i .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EgJcyjYseWwexE7i .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EgJcyjYseWwexE7i .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EgJcyjYseWwexE7i :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不匹配
匹配 user-service
客户端
路由断言
Path=/user/**
下一条路由
Path=/order/**
过滤器 1
过滤器 2
过滤器 N
user-service
8081/8082
过滤器 N 处理响应
过滤器 2 处理响应
过滤器 1 处理响应
关键在"双向"这两个字:GatewayFilter 既能改进入网关的请求 (请求头、请求参数、路径),也能改微服务返回的响应(响应头、状态码)。讲义对它的定义是"GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理",这句话把职责边界画得很清楚。
断言和过滤器的分工,用一张表直接对比:
| 维度 | 路由断言 Predicate | 路由过滤器 GatewayFilter |
|---|---|---|
| 回答的问题 | 请求要不要走这条路由 | 走过去时怎么加工请求/响应 |
| 配置位置 | routes[].predicates |
routes[].filters 或 default-filters |
| 作用时机 | 路由匹配阶段 | 路由确定之后、请求转发前后 |
| 能否改内容 | 不能,只做判断 | 能,改头/改参数/改路径/改状态码 |
| 命中失败的后果 | 换下一条路由,全不中则 404 | 不影响匹配,只影响加工结果 |
两类作用域:路由过滤器与默认过滤器
Spring Cloud Gateway 内置了 31 种路由过滤器工厂,但它们按配置位置分两类,作用范围完全不同,这是本篇的骨架。
text
spring:
cloud:
gateway:
routes: # ---------- 路由列表 ----------
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 路由过滤器:只对 user-service 这条路由生效
- AddRequestHeader=Truth,Itcast is freaking awesome!
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters: # 默认过滤器:对所有路由生效(与 routes 同级)
- AddRequestHeader=Truth,Itcast is freaking awesome!
对照表格:
| 维度 | 路由过滤器(局部) | 默认过滤器 default-filters |
|---|---|---|
| 配置位置 | routes[].filters |
spring.cloud.gateway.default-filters,与 routes 同级 |
| 生效范围 | 只对当前这一条路由 | 对所有路由都生效 |
| 典型用途 | 某条路由要特殊改造(改路径、加专属头) | 全站统一改造(统一鉴权头、统一响应头) |
| 会不会重复 | 不会 | 会与 route filters 叠加执行 |
| 与 GlobalFilter 的关系 | 完全无关,都是配置式的 | 仍然不是 GlobalFilter,两者不是一回事 |
最后一行要单独说。很多资料把 default-filters 叫成"全局过滤器",这是错的。真正的 GlobalFilter 是一个编程式组件 ,需要你自己写类去实现 GlobalFilter 和 Ordered 接口,写 Java 代码决定业务逻辑;而 default-filters 只是把配置式过滤器的作用域从"一条路由"放宽到"所有路由",逻辑仍然是 Spring 内置工厂的固定逻辑,你改不了它干什么。想拦截请求做自己的业务(比如鉴权、限流判定),default-filters 做不到,必须写 GlobalFilter。GlobalFilter 是下一篇的内容,这里先把边界划清。
前面给的 yaml 不是编的,是 cloud-demo 工程终态里真实的网关配置,两个讲义阶段的工程写法一致:
yaml
server:
port: 10010
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
application:
name: gateway
cloud:
nacos:
server-addr: nacos:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标示,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=Truth,Itcast is freaking awesome!
讲义里同一件事的写法(3.4.2 请求头过滤器 + 3.4.3 默认过滤器)是先局部后全局两步走:
yaml
# 只对 userservice 生效的局部写法
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头
yaml
# 改写到 default 下,对所有路由生效
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, Itcast is freaking awesome!
注意工程里是 AddRequestHeader=Truth,Itcast is freaking awesome!(逗号后没有空格),讲义里是 AddRequestHeader=Truth, Itcast is freaking awesome!(逗号后有一个空格)。两种都能跑,因为逗号才是参数分隔符,空格会被当作值的一部分。但如果你希望下游拿到的是干净的 Itcast is freaking awesome!,就别在逗号后加空格------加了之后值就变成 Itcast is freaking awesome!(前面多一个空格)。工程里没加空格是有意为之。
常用过滤器工厂
AddRequestHeader:加请求头
最典型也最有价值的用途是网关鉴权后把用户身份透传给下游 。网关统一校验 token,校验通过后把解析出的 userId 塞进请求头,下游微服务直接从请求头拿身份,各自不用再解析一遍 token,也不用把鉴权逻辑复制到每个服务。课程里的示例更简单,加一个固定头 Truth:
yaml
filters:
- AddRequestHeader=Truth,Itcast is freaking awesome!
格式是 AddRequestHeader=<头名>,<头值>。头名 Truth,头值 Itcast is freaking awesome!。
AddRequestParameter:加请求参数
给下游请求追加查询参数,格式 AddRequestParameter=<参数名>,<参数值>:
yaml
filters:
- AddRequestParameter=red,blue
下游最终看到的是 ?red=blue。
AddResponseHeader:加响应头
加工的是返回路径上的响应头,格式 AddResponseHeader=<头名>,<头值>:
yaml
filters:
- AddResponseHeader=X-Response-Red,Blue
常用于统一加跨域头、链路追踪 ID、版本标识之类。
StripPrefix:去掉路径前缀
Gateway 暴露给外部的路径,往往和下游服务真实的接口路径不一致,这时候要把多出来的前缀剥掉。StripPrefix 的参数是去掉的路径段数,不是前缀字符串。
假设网关对外暴露 /api/user/1,而 user-service 的接口是 /user/{id},那么 /api 这一段要去掉:
yaml
filters:
- StripPrefix=1
请求前后路径对比:
| 阶段 | 路径 | 说明 |
|---|---|---|
| 客户端请求 | /api/user/1 |
对外路径,带 /api 前缀 |
StripPrefix=1 之后 |
/user/1 |
去掉 1 段,正好命中下游 /user/{id} |
| 下游收到 | /user/1 |
与 Controller 的 @RequestMapping("/user") + @GetMapping("/{id}") 对上 |
这里有个必须说清的点:cloud-demo 工程里 gateway 的路由是 - Path=/user/**,转发后路径原样保持 /user/1,而 user-service 的 Controller 恰好也是 @RequestMapping("/user") 下的 @GetMapping("/{id}"),两边天然对齐。这种情况下不要配 StripPrefix 。如果手滑加了 - StripPrefix=1,路径会变成 /1,下游找不到映射,直接 404。StripPrefix 是给"网关路径比下游多一层"的场景用的,只在真有多余前缀时才加。
PrefixPath:加路径前缀
和 StripPrefix 反向操作,给转发给下游的路径前面补一段:
yaml
filters:
- PrefixPath=/user
配了它之后,客户端请求 /1,下游实际收到 /user/1。用于网关省略前缀、由网关补齐的场景。
RewritePath:正则改写路径
前缀增删不够用的时候用正则改写,格式 RewritePath=<匹配正则>,<替换表达式>,配合命名捕获组:
yaml
filters:
- RewritePath=/api/?(?<segment>.*), /$\{segment}
(?<segment>.*) 把 /api/ 后面的部分捕获下来,替换表达式中用 ${segment} 引用。注意 yaml 里要写成 $\{segment} ,那个 \ 是必须的,Spring 在解析属性占位符时要靠它区分,只写 ${segment} 会被当成配置占位符而报错。这个写法跟 StripPrefix 效果类似,但更灵活------可以只改中间某一段,或者做新旧接口路径的兼容映射。
SetStatus:设置响应状态码
直接改写返回给客户端的 HTTP 状态码:
yaml
filters:
- SetStatus=401
常用于按路由维度做灰度或降级,比如某条路由临时下线,直接把状态改成 503。
速查表
| 工厂名 | 作用 | 参数 | 示例 |
|---|---|---|---|
AddRequestHeader |
给下游请求加请求头 | 头名,头值 |
- AddRequestHeader=Truth,Itcast is freaking awesome! |
RemoveRequestHeader |
移除请求头 | 头名 |
- RemoveRequestHeader=Cookie |
AddRequestParameter |
给下游请求加查询参数 | 参数名,参数值 |
- AddRequestParameter=red,blue |
AddResponseHeader |
给响应加响应头 | 头名,头值 |
- AddResponseHeader=X-Response-Red,Blue |
RemoveResponseHeader |
移除响应头 | 头名 |
- RemoveResponseHeader=X-Request-Id |
StripPrefix |
去掉路径前 N 段 | 段数 |
- StripPrefix=1 |
PrefixPath |
给路径加前缀 | 前缀 |
- PrefixPath=/user |
RewritePath |
正则改写路径 | 正则,替换表达式 |
- RewritePath=/api/?(?<segment>.*), /$\{segment} |
SetStatus |
设置响应状态码 | 状态码 |
- SetStatus=401 |
RequestRateLimiter |
限制请求流量 | 限流器参数 | - RequestRateLimiter=... |
讲义 3.4.1 列出的示例清单是 AddRequestHeader、RemoveRequestHeader、AddResponseHeader、RemoveResponseHeader、RequestRateLimiter,一共 31 种,不用全背;名字基本能自解释,真用到的时候去官网查具体语法即可。
参数写法:- Name=参数 的格式细节
这一块的坑最多,先把正确写法摆出来:
yaml
filters:
- AddRequestHeader=Truth,Itcast is freaking awesome!
拆开看:
- 列表里每个过滤器是一个字符串 ,
-后面整体是Name=参数形式,不是嵌套的 map。 - 参数之间用英文逗号 分隔,
Truth是第一个参数,Itcast is freaking awesome!是第二个。 AddRequestHeader和Truth之间的=两边不要有空格 。写成AddRequestHeader = Truth,...会让=前多出空格,过滤器名解析不出来,配置直接不生效。
正确的和错误的对照:
| 写法 | 结果 |
|---|---|
- AddRequestHeader=Truth,Itcast is freaking awesome! |
✅ 正常,头名 Truth |
- AddRequestHeader = Truth, Itcast is freaking awesome! |
❌ = 前有空格,过滤器名解析失败 |
- AddRequestHeader: Truth, Itcast is freaking awesome! |
❌ 用冒号写成 map,不生效 |
- AddRequestHeader=Truth;Itcast is freaking awesome! |
❌ 用分号而非逗号,参数分不开 |
- AddRequestHeader=Truth,Itcast,is,freaking,awesome! |
❌ 值里含逗号,被截成多个参数,值只剩 Itcast |
最后一行是最隐蔽的坑:值本身如果带逗号,会被当成参数分隔符截断 。AddRequestHeader 的头值想带逗号,这个写法就撑不住了,得改用编程式过滤器在代码里拼。请求头的值里带逗号的场景并不罕见(比如 Accept: text/html,application/json),配置式写法在这里是有天花板的。
动手验证
验证分两半:下游要能回显收到的请求头,网关侧改配置重启,然后 curl 观察。
工程结构上,改的是 gateway 模块的 application.yml,回显接口加在 user-service 模块:
text
cloud-demo/
├── gateway/ # 网关服务,端口 10010
│ └── src/main/resources/
│ └── application.yml # 改这里:filters / default-filters
├── user-service/ # 用户服务,端口 8081
│ └── src/main/java/cn/itcast/user/web/
│ └── UserController.java # 加这里的请求头回显
├── order-service/ # 订单服务
├── feign-api/
└── eureka-server/
user-service 的 UserController 加一个 @RequestHeader 参数,把网关加的头接出来打印:
java
package cn.itcast.user.web;
import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties properties;
@GetMapping("now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
}
/**
* 路径: /user/110
*
* @param id 用户id
* @param truth 网关透传的请求头,可能为空
*/
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println("truth: " + truth);
return userService.queryById(id);
}
}
required = false 是必须的------不是所有请求都会带这个头,不写的话上游没传就抛 MissingRequestHeaderException。
网关配置确认成 default-filters 版本(就是对所有路由生效那一份),重启 gateway 和 user-service,然后走网关发请求:
bash
# 走网关访问用户服务,网关会补上 Truth 请求头
curl -i http://localhost:10010/user/1
预期:
| 观察点 | 预期结果 |
|---|---|
| 响应状态 | 200 OK,正常返回用户 JSON |
| user-service 控制台 | 打印 truth: Itcast is freaking awesome! |
再访问 http://localhost:8082/user/2(另一实例) |
同样打印,证明 default-filters 对所有实例、所有路由都生效 |
直连 http://localhost:8081/user/1(绕过网关) |
打印 truth: null,证明头是网关加的,不是客户端带的 |
最后一行是排查的照妖镜:如果直连也打印出了值,说明头不是网关加的,可能是客户端自己带的。想再验证"客户端给的头会被覆盖"这一条,可以这样测:
bash
# 客户端自己带一个 Truth 头,看网关是保留还是覆盖
curl -i -H "Truth: client-value" http://localhost:10010/user/1
# 控制台仍然是 Itcast is freaking awesome!,网关的头覆盖了客户端传的
想验证响应头一侧,把配置换成 AddResponseHeader 再 curl 加 -i 看响应头:
yaml
default-filters:
- AddResponseHeader=X-Gateway,cloud-demo
bash
curl -i http://localhost:10010/user/1 | grep -i x-gateway
# 预期输出:x-gateway: cloud-demo
执行顺序
同一条路由的 filters 列表里配了多个过滤器时,按声明顺序执行 ,写在上面先执行,写在下面后执行。这一点直接影响结果------比如你先 AddRequestHeader 再 RemoveRequestHeader 同一个头,那个头就会被删掉;顺序反了就删不掉。
至于 default-filters 和 route filters 的先后关系,讲义 3.5.3 过滤器执行顺序 给的口径是:
- 请求进入网关会碰到三类过滤器:当前路由的过滤器 、DefaultFilter 、GlobalFilter。
- 三类会合并到一条过滤器链里排序后依次执行,每个过滤器带一个 int 型 order,order 越小越先执行。
- 路由过滤器和 DefaultFilter 的 order 由 Spring 指定,默认按声明顺序从 1 递增。
- 当 order 值相同时,执行顺序是 defaultFilter > 路由过滤器 > GlobalFilter。
源码层面:RouteDefinitionRouteLocator#getFilters() 先加载 defaultFilters、再加载某条 route 的 filters,然后合并;FilteringWebHandler#handle() 再把 GlobalFilter 合进来统一按 order 排序。完整的排序规则和 GlobalFilter 的 order 指定方式放在下一篇(过滤器链执行顺序)展开,这里先记住结论。
实战坑
StripPrefix=1的数字含义算错 。这个1是"去掉几段路径",不是"去掉第 1 段"。/api/user/1配StripPrefix=1得到/user/1,配StripPrefix=2得到/1。工程里 gateway 路径/user/**与下游/user/{id}天然对齐,根本不该配 StripPrefix,多配一次就是 404。AddRequestHeader的值里带逗号 。逗号是参数分隔符,AddRequestHeader=A,B,C会被拆成三个参数,实际只有前两个生效,值只剩B。请求头带逗号的场景(Accept、Cache-Control)必须改用编程式过滤器。- default-filters 和 route filters 配了同名过滤器 。两边都生效,可能给同一个头加两遍;
AddRequestHeader后一次会覆盖前一次的同名头,看着像没生效,实际是顺序问题。排查时先 grep 一遍两处配置,确认没有重复声明。 - yml 缩进写错 。
filters必须和predicates同级缩进在 route 下面,default-filters必须和routes同级缩进在gateway下面。缩进错一层,Spring 直接当未知属性忽略,不报错也不生效,最容易耗时间。改完配置记得看启动日志里有没有绑定异常。 - 把 GatewayFilter 和 GlobalFilter 混为一谈 。
default-filters配的还是内置的 GatewayFilter 工厂,逻辑固定;想写自己的鉴权/限流逻辑只能实现GlobalFilter。两者合并进同一条链,但来源、写法、能力都不同。 - 配置改了没重启 。gateway 的 yml 不是热加载的,
filters改动必须重启网关进程。验证前先确认重启了,否则会误判成"配置不生效"。
API 速览
| 配置项 | 位置 | 说明 |
|---|---|---|
spring.cloud.gateway.routes[].filters |
gateway 的 application.yml |
路由过滤器列表,只对当前路由生效 |
spring.cloud.gateway.default-filters |
与 routes 同级 |
默认过滤器列表,对所有路由生效 |
AddRequestHeader=name,value |
过滤器参数 | 给下游请求加请求头 |
AddRequestParameter=name,value |
过滤器参数 | 给下游请求加查询参数 |
AddResponseHeader=name,value |
过滤器参数 | 给响应加响应头 |
StripPrefix=n |
过滤器参数 | 去掉路径前 n 段 |
PrefixPath=/prefix |
过滤器参数 | 给路径加前缀 |
RewritePath=regex,replacement |
过滤器参数 | 正则改写路径,注意转义 $\{...} |
SetStatus=code |
过滤器参数 | 设置响应状态码 |
@RequestHeader(value="Truth", required=false) |
下游 Controller | 接收网关透传的请求头 |
官方文档
总结
- GatewayFilter 处理的是双向链路:请求进网关时能改,微服务返回响应时也能改。
- 作用域就两种。写在
routes[].filters下只对当前路由生效,写在default-filters下对所有路由生效。default-filters不是 GlobalFilter,前者是配置式固定逻辑,后者要自己写代码。 AddRequestHeader的主要价值是网关鉴权后向下游透传用户身份;StripPrefix解决网关路径与下游路径不一致的问题,段数算错就是 404。- 参数格式是
- Name=参数,逗号分隔、=两侧不留空格、值里不能有逗号。 - 同一条路由内多个过滤器按声明顺序执行;default-filters 与 route filters 会合并排序,同 order 时 defaultFilter 在前,完整规则见下一篇。