概述
前面用 AddRequestHeader 这类过滤器工厂做的都是配置式的活儿------加个头、改个路径参数还行,一旦要求"校验 token、判断有没有权限访问、统计接口耗时",配置文件就写不出来了。全局过滤器就是给这种自定义逻辑留的口子:代码想怎么拦就怎么拦,而且对所有路由无条件生效。
纲要
- 配置式过滤器的天花板:Spring 内置 31 种工厂,逻辑写死,参数可配、逻辑不可改
GlobalFilter与GatewayFilter的本质区别与执行关系default-filters到底是不是全局过滤器(这里最容易混淆)- 动手:
AuthorizeFilter implements GlobalFilter, Ordered完整实现 filter(ServerWebExchange, GatewayFilterChain)的两个参数各自干什么getOrder()为什么给负数:鉴权必须排在链路最前面- 响应式 API 的两个致命坑:漏
return请求挂死、拦截忘setComplete()响应不结束 - Gateway 为什么基于 WebFlux,以及网关里写阻塞代码的后果
- curl 两次请求验证 200 / 401
- 多个 GlobalFilter 的排序规则与实战踩坑清单
配置式过滤器的天花板
Spring Cloud Gateway 自带 31 种路由过滤器工厂,常用的那几个:
| 工厂名 | 作用 |
|---|---|
AddRequestHeader |
给请求加一个请求头 |
RemoveRequestHeader |
删除请求里的某个请求头 |
AddResponseHeader |
给响应加一个响应头 |
RemoveResponseHeader |
从响应里删掉某个响应头 |
RequestRateLimiter |
限流 |
RewritePath |
重写请求路径 |
它们有两个共同点,也正是局限所在:
- 只能配参数,不能写逻辑。工厂背后的处理代码是 Spring 提前写死的,你在 yml 里能改的只有传进去的参数值(比如头名叫什么、值是多少)。
- 配置在哪一级,作用范围就锁死在哪一级 。写在某个
routes[].filters下只对该路由生效,写在default-filters下才对全部路由生效。
对照表更直观:
| 维度 | 配置式过滤器(GatewayFilter 工厂 / default-filters) | 编程式过滤器(GlobalFilter) |
|---|---|---|
| 定义方式 | 在 application.yml 里配 |
写一个 Java 类实现接口 |
| 处理逻辑 | Spring 内置,写死 | 完全自定义 |
| 可配内容 | 只能传工厂限定的参数 | 想干什么干什么 |
| 生效范围 | 路由级 / 全路由(配置决定) | 全局,所有路由无差别生效 |
| 典型用途 | 加请求头、改路径、限流参数 | 鉴权、校验 token、记录耗时、改写请求体 |
| 需要重启改代码 | 改 yml 即可 | 改代码重新编译部署 |
所以需求一旦变成"判断这个请求是谁发的、有没有权限访问",配置这条路就走不通了------权限判断的逻辑不可能交给 Spring 写死。得自己写代码,这就是 GlobalFilter 的用武之地。
GlobalFilter 与 GatewayFilter 的关系
先把两个接口摆正:
| 接口 | 归属 | 生效方式 |
|---|---|---|
GatewayFilter |
网关内置工厂的接口,通过配置绑定 | 绑到某个路由,或绑到 default-filters |
GlobalFilter |
自己实现的接口 | 无条件作用于所有路由 |
关键认知:两者的执行时机在同一个过滤器链里。 请求进网关、路由匹配完成之后,Gateway 会把三类过滤器合并进一个集合,按 order 排序,然后依次执行:
- 当前路由上配置的路由过滤器(
GatewayFilter) default-filters里的默认过滤器(本质还是GatewayFilter)- 所有
GlobalFilter
#mermaid-svg-65fLozJYdEIOaG7C{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-65fLozJYdEIOaG7C .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-65fLozJYdEIOaG7C .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-65fLozJYdEIOaG7C .error-icon{fill:#552222;}#mermaid-svg-65fLozJYdEIOaG7C .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-65fLozJYdEIOaG7C .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-65fLozJYdEIOaG7C .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-65fLozJYdEIOaG7C .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-65fLozJYdEIOaG7C .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-65fLozJYdEIOaG7C .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-65fLozJYdEIOaG7C .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-65fLozJYdEIOaG7C .marker{fill:#333333;stroke:#333333;}#mermaid-svg-65fLozJYdEIOaG7C .marker.cross{stroke:#333333;}#mermaid-svg-65fLozJYdEIOaG7C svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-65fLozJYdEIOaG7C p{margin:0;}#mermaid-svg-65fLozJYdEIOaG7C .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-65fLozJYdEIOaG7C .cluster-label text{fill:#333;}#mermaid-svg-65fLozJYdEIOaG7C .cluster-label span{color:#333;}#mermaid-svg-65fLozJYdEIOaG7C .cluster-label span p{background-color:transparent;}#mermaid-svg-65fLozJYdEIOaG7C .label text,#mermaid-svg-65fLozJYdEIOaG7C span{fill:#333;color:#333;}#mermaid-svg-65fLozJYdEIOaG7C .node rect,#mermaid-svg-65fLozJYdEIOaG7C .node circle,#mermaid-svg-65fLozJYdEIOaG7C .node ellipse,#mermaid-svg-65fLozJYdEIOaG7C .node polygon,#mermaid-svg-65fLozJYdEIOaG7C .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-65fLozJYdEIOaG7C .rough-node .label text,#mermaid-svg-65fLozJYdEIOaG7C .node .label text,#mermaid-svg-65fLozJYdEIOaG7C .image-shape .label,#mermaid-svg-65fLozJYdEIOaG7C .icon-shape .label{text-anchor:middle;}#mermaid-svg-65fLozJYdEIOaG7C .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-65fLozJYdEIOaG7C .rough-node .label,#mermaid-svg-65fLozJYdEIOaG7C .node .label,#mermaid-svg-65fLozJYdEIOaG7C .image-shape .label,#mermaid-svg-65fLozJYdEIOaG7C .icon-shape .label{text-align:center;}#mermaid-svg-65fLozJYdEIOaG7C .node.clickable{cursor:pointer;}#mermaid-svg-65fLozJYdEIOaG7C .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-65fLozJYdEIOaG7C .arrowheadPath{fill:#333333;}#mermaid-svg-65fLozJYdEIOaG7C .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-65fLozJYdEIOaG7C .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-65fLozJYdEIOaG7C .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-65fLozJYdEIOaG7C .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-65fLozJYdEIOaG7C .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-65fLozJYdEIOaG7C .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-65fLozJYdEIOaG7C .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-65fLozJYdEIOaG7C .cluster text{fill:#333;}#mermaid-svg-65fLozJYdEIOaG7C .cluster span{color:#333;}#mermaid-svg-65fLozJYdEIOaG7C 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-65fLozJYdEIOaG7C .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-65fLozJYdEIOaG7C rect.text{fill:none;stroke-width:0;}#mermaid-svg-65fLozJYdEIOaG7C .icon-shape,#mermaid-svg-65fLozJYdEIOaG7C .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-65fLozJYdEIOaG7C .icon-shape p,#mermaid-svg-65fLozJYdEIOaG7C .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-65fLozJYdEIOaG7C .icon-shape .label rect,#mermaid-svg-65fLozJYdEIOaG7C .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-65fLozJYdEIOaG7C .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-65fLozJYdEIOaG7C .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-65fLozJYdEIOaG7C :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 放行 chain.filter
放行
拦截 setComplete
客户端请求
RoutePredicateHandlerMapping 匹配路由
FilteringWebHandler 收集过滤器
defaultFilters + 路由 filters + 所有 GlobalFilter
按 order 升序排序成一条链
Filter order=-1 AuthorizeFilter 鉴权
Filter order=1 AddRequestHeader
Filter order=2 路由级过滤器...
NettyRoutingFilter 转发到真实服务
微服务处理并返回
响应按 order 逆序回穿过滤器
客户端拿到响应
直接返回 401 不再往下走
这条链是两种过滤器的合流点 ,GlobalFilter 不是"凌驾于"GatewayFilter 之上的另一套机制,只是它进链子的方式不需要配置、以及 order 由我们自己指定。源码里合并和排序发生在 org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()。
收口一个老混淆:default-filters 不是 GlobalFilter
上一篇讲 default-filters 时用过一个说法------"它和全局过滤器作用一样,都对所有路由生效"。这句话只对了一半,得掰开:
- 作用范围确实一样 :
default-filters和GlobalFilter都作用于所有路由。 - 本质完全不同 :
default-filters里配的还是GatewayFilter工厂,逻辑依旧写死,只是把作用范围从单个路由扩大到了全部路由;它的 order 由 Spring 按声明顺序从 1 自增分配,你改不了。GlobalFilter才是真正的自定义逻辑 + 自定义 order。
一句话:default-filters = 配置式的"全局生效的 GatewayFilter",GlobalFilter = 编程式的全局过滤器。名字都带"全局"或"默认",但一个是配置、一个是代码,别混。
动手:写一个鉴权全局过滤器
需求很朴素,但足够说明问题:拦截所有进入网关的请求,检查请求参数里有没有 authorization,值是不是 admin,同时满足就放行,否则返回 401。
工程目录结构(SpringCloud02/代码/cloud-demo/cloud-demo/gateway,注意 cloud-demo 多嵌套了一层):
tree
gateway
├── pom.xml
└── src/main
├── java/cn/itcast/gateway
│ ├── GatewayApplication.java # 启动类
│ └── AuthorizeFilter.java # 全局鉴权过滤器
└── resources
└── application.yml # 路由 + default-filters 配置
cn.itcast.gateway.AuthorizeFilter 是工程终态里真实存在并落地 的类(SpringCloud02 与 day01-SpringCloud01 两份代码里都有),下面原样引用:
java
package cn.itcast.gateway;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
// @Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
// 2.获取参数中的 authorization 参数
String auth = params.getFirst("authorization");
// 3.判断参数值是否等于 admin
if ("admin".equals(auth)) {
// 4.是,放行
return chain.filter(exchange);
}
// 5.否,拦截
// 5.1.设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 5.2.拦截请求
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return -1;
}
}
两个细节值得单独说。
接口只有一个方法。 GlobalFilter 的定义极简,就一个 filter:
java
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过 GatewayFilterChain 把请求交给下一个过滤器
*
* @param exchange 请求上下文,可以获取 Request、Response 等信息,也能在整条链路里存取数据
* @param chain 过滤器链,调用它的 filter 方法即表示放行给下一个过滤器
* @return Mono<Void> 表示当前过滤器的业务处理完成
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
exchange 是请求上下文 :从请求进网关开始到响应结束为止,整条链路共享同一个对象,里面能拿请求、拿响应,也能 getAttributes().put(...) 存东西给后面的过滤器读。chain 是过滤器链 :调用 chain.filter(exchange) 就是从链里找下一个过滤器并执行它,等价于"放行"。
顺序注解和 Ordered 接口二选一。 顺带说明,工程源码里保留了两种指定 order 的写法,只是注掉了注解那行:
java
// 写法一:注解,写在类上
// @Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter { ... }
// 写法二:实现 Ordered 接口(工程最终采用这种)
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return -1;
}
}
@Order 需要一个 int 值,Ordered 接口的方法名是 getOrder(),返回值也是 int,两种写法效果完全等价。工程里 @Order(-1) 被注掉、保留了 Ordered 接口版本。
getOrder() 为什么给 -1
过滤器的 order 规则只有一条:值越小优先级越高,越先执行。
Integer.MAX_VALUE(2147483647)是默认值,等于优先级最低;- 负数优先级高于所有默认值的过滤器。
AuthorizeFilter 是鉴权,必须在做任何业务处理之前跑------如果排在日志过滤器后面,就会出现"请求已经被处理并打了日志,然后才被拦下来"的荒唐局面。所以给它一个负数 -1,保证它在链路最前端。千万不要写 0 或忘写,一旦有别的过滤器也用默认值,顺序就不确定了。
响应式 API 的两个坑
Gateway 网关底层是 Spring WebFlux ,不是我们熟悉的 Spring MVC。exchange.getRequest() 拿到的 ServerHttpRequest、exchange.getResponse() 拿到的 ServerHttpResponse 都是响应式的,没有 Servlet 那套 HttpServletRequest / HttpServletResponse。以前写 Filter 顺手就来的这些写法在这里全是错的:
java
// 错误示范:网关里没有这些 Servlet API,编译就过不去
response.getWriter().write("unauthorized");
response.sendRedirect("/login");
chain.doFilter(request, response);
对应到全局过滤器,有两个错误几乎每个初学者都会踩一次。
坑一:漏了 return,请求直接挂死
chain.filter(exchange) 的返回值是一个 Mono<Void>,必须把它作为 filter 方法的返回值返回 ,不能只是"调一下"。因为 WebFlux 是惰性的,Mono 不被返回、不被订阅,后面的路由逻辑根本不会执行,客户端就一直是"请求中"的状态,直到超时。
java
// 错误:调了但没返回,请求挂住不响应
chain.filter(exchange);
// 正确:把 Mono 返回出去,链子才继续往下走
return chain.filter(exchange);
报错现象不是抛异常,而是请求永远 pending、最后超时------这类问题没有堆栈可查,只能靠代码审查。
坑二:拦截时忘了 setComplete()
放行靠 return chain.filter(exchange),拦截靠 return exchange.getResponse().setComplete()。setComplete() 的作用是结束响应 ,它自己也返回一个 Mono<Void>,同样要 return。
java
// 错误:只设了状态码,没有结束响应,客户端等不到响应体
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 正确:设置状态码 + 结束响应,两步都做
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
只设状态码不 setComplete(),网关的响应流不会关闭,浏览器一样是转圈到超时。
另外提一句状态码口径。讲义示例里用的是 HttpStatus.FORBIDDEN(403,禁止访问),工程终态落地时改成了 HttpStatus.UNAUTHORIZED(401,未认证/未登录)。鉴权场景下 401 更贴切------"你没带凭证"是未认证,403 是"你认证了但没权限"。本文以工程终态为准用 401。
顺带说清:网关不能引 spring-boot-starter-web
Gateway 基于 WebFlux 响应式编程,它的模块里如果引入了 spring-boot-starter-web,直接会启动报错或行为异常 ------因为那会拉起 Tomcat + Spring MVC,与 Netty + WebFlux 冲突。看工程 gateway 模块的依赖,只有两个:
xml
<dependencies>
<!--nacos服务注册发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--网关gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
spring-cloud-starter-gateway 自带 WebFlux(Netty),没有 spring-boot-starter-web。这也是新手最常见的启动报错来源。
由此引出一个实战禁区:网关里绝对不能写阻塞代码 。WebFlux 用少量线程(默认就等于 CPU 核数)承载所有请求,一个过滤器里如果出现 JDBC 查询、Thread.sleep、同步 HTTP 调用这类阻塞操作,就会把这个线程占死------AuthorizeFilter 里做个数据库查 token,几十个并发就能把整个网关拖垮,因为可用线程全被堵住了。网关里要查外部数据,得用响应式客户端(如 WebClient),不能走 JDBC。
验证:curl 两次请求
启动 Nacos、user-service、gateway(端口 10010)后,用 curl 分别带和不带 authorization 参数访问,观察状态码。
bash
# 1. 不带 authorization 参数,预期 401
curl -i http://localhost:10010/user/1
# 2. 带上 authorization=admin,预期 200,返回用户数据
curl -i "http://localhost:10010/user/1?authorization=admin"
预期结果:
| 请求 | HTTP 状态码 | 说明 |
|---|---|---|
GET /user/1 |
401 Unauthorized |
params.getFirst("authorization") 返回 null,不满足 "admin".equals(auth),被拦截 |
GET /user/1?authorization=admin |
200 OK |
校验通过,chain.filter(exchange) 放行,转发到 user-service |
GET /user/1?authorization=xxx |
401 Unauthorized |
值不等于 admin,同样拦截 |
curl -i 会打印响应头,第二行就是状态码,比浏览器 F12 更直接。
多个全局过滤器的排序
一个项目不可能只有一个过滤器------鉴权、日志、限流往往各写一个。它们的执行顺序由 order 决定:
#mermaid-svg-DyCjqtZBtYXiZC0B{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-DyCjqtZBtYXiZC0B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DyCjqtZBtYXiZC0B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DyCjqtZBtYXiZC0B .error-icon{fill:#552222;}#mermaid-svg-DyCjqtZBtYXiZC0B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DyCjqtZBtYXiZC0B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DyCjqtZBtYXiZC0B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DyCjqtZBtYXiZC0B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DyCjqtZBtYXiZC0B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DyCjqtZBtYXiZC0B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DyCjqtZBtYXiZC0B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DyCjqtZBtYXiZC0B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DyCjqtZBtYXiZC0B .marker.cross{stroke:#333333;}#mermaid-svg-DyCjqtZBtYXiZC0B svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DyCjqtZBtYXiZC0B p{margin:0;}#mermaid-svg-DyCjqtZBtYXiZC0B .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DyCjqtZBtYXiZC0B .cluster-label text{fill:#333;}#mermaid-svg-DyCjqtZBtYXiZC0B .cluster-label span{color:#333;}#mermaid-svg-DyCjqtZBtYXiZC0B .cluster-label span p{background-color:transparent;}#mermaid-svg-DyCjqtZBtYXiZC0B .label text,#mermaid-svg-DyCjqtZBtYXiZC0B span{fill:#333;color:#333;}#mermaid-svg-DyCjqtZBtYXiZC0B .node rect,#mermaid-svg-DyCjqtZBtYXiZC0B .node circle,#mermaid-svg-DyCjqtZBtYXiZC0B .node ellipse,#mermaid-svg-DyCjqtZBtYXiZC0B .node polygon,#mermaid-svg-DyCjqtZBtYXiZC0B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DyCjqtZBtYXiZC0B .rough-node .label text,#mermaid-svg-DyCjqtZBtYXiZC0B .node .label text,#mermaid-svg-DyCjqtZBtYXiZC0B .image-shape .label,#mermaid-svg-DyCjqtZBtYXiZC0B .icon-shape .label{text-anchor:middle;}#mermaid-svg-DyCjqtZBtYXiZC0B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DyCjqtZBtYXiZC0B .rough-node .label,#mermaid-svg-DyCjqtZBtYXiZC0B .node .label,#mermaid-svg-DyCjqtZBtYXiZC0B .image-shape .label,#mermaid-svg-DyCjqtZBtYXiZC0B .icon-shape .label{text-align:center;}#mermaid-svg-DyCjqtZBtYXiZC0B .node.clickable{cursor:pointer;}#mermaid-svg-DyCjqtZBtYXiZC0B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DyCjqtZBtYXiZC0B .arrowheadPath{fill:#333333;}#mermaid-svg-DyCjqtZBtYXiZC0B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DyCjqtZBtYXiZC0B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DyCjqtZBtYXiZC0B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DyCjqtZBtYXiZC0B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DyCjqtZBtYXiZC0B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DyCjqtZBtYXiZC0B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DyCjqtZBtYXiZC0B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DyCjqtZBtYXiZC0B .cluster text{fill:#333;}#mermaid-svg-DyCjqtZBtYXiZC0B .cluster span{color:#333;}#mermaid-svg-DyCjqtZBtYXiZC0B 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-DyCjqtZBtYXiZC0B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DyCjqtZBtYXiZC0B rect.text{fill:none;stroke-width:0;}#mermaid-svg-DyCjqtZBtYXiZC0B .icon-shape,#mermaid-svg-DyCjqtZBtYXiZC0B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DyCjqtZBtYXiZC0B .icon-shape p,#mermaid-svg-DyCjqtZBtYXiZC0B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DyCjqtZBtYXiZC0B .icon-shape .label rect,#mermaid-svg-DyCjqtZBtYXiZC0B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DyCjqtZBtYXiZC0B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DyCjqtZBtYXiZC0B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DyCjqtZBtYXiZC0B :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 鉴权 getOrder=-1
日志 getOrder=0
限流 getOrder=1
路由转发
排序规则(来自 FilteringWebHandler):
| 规则 | 内容 |
|---|---|
| 基本规则 | order 值越小,优先级越高,越靠前执行 |
| GlobalFilter 的 order | 自己指定(@Order 或 Ordered 接口) |
| 路由过滤器 / defaultFilters 的 order | Spring 指定,默认按声明顺序从 1 自增 |
| order 相同的冲突 | 按 defaultFilter > 路由过滤器 > GlobalFilter 执行 |
最后一条要留意:多个 GlobalFilter 如果 order 都写成一样的值,它们的相对顺序就是不确定的(同一优先级内按收集顺序,但收集顺序不该被依赖)。真要保证顺序,就给每个全局过滤器一个唯一的 order。
实战坑清单
chain.filter(exchange)没 return:请求挂死、无异常无堆栈,最后超时。- 拦截时忘
setComplete():只设了状态码,响应流不关,客户端一直转圈。 getOrder()返回 0 或默认值:鉴权排在日志后面,出现"先处理再拦截"的诡异时序。- 在过滤器里写阻塞调用 (
Thread.sleep/ JDBC / 同步 HTTP):占死 WebFlux 少量线程,并发一上来整个网关性能崩塌。 - 多个 GlobalFilter 的 order 相同:执行顺序不确定,行为随机。
- 想改请求体 :比改请求头麻烦得多,需要包装
ServerHttpRequestDecorator重写getBody(),涉及流式读取和数据缓冲,本文不展开,但心里得有数------响应式里请求体只能读一次的坑就在这里。 - 网关模块误引
spring-boot-starter-web:与 WebFlux 冲突,启动即失败。
API 速览
| API | 说明 |
|---|---|
GlobalFilter#filter(ServerWebExchange, GatewayFilterChain) |
全局过滤器唯一方法,实现自定义逻辑 |
exchange.getRequest() |
获取 ServerHttpRequest,可读路径、参数、请求头、请求体 |
request.getQueryParams() |
获取查询参数,返回 MultiValueMap<String, String> |
params.getFirst(name) |
取同名参数的第一个值,比 get() 更常用 |
exchange.getResponse() |
获取 ServerHttpResponse |
response.setStatusCode(HttpStatus) |
设置响应状态码 |
response.setComplete() |
结束响应(返回 Mono<Void>,拦截时须 return) |
chain.filter(exchange) |
放行给下一个过滤器(返回 Mono<Void>,须 return) |
Ordered#getOrder() / @Order(int) |
指定 order,值越小优先级越高 |
官方文档
总结
- 配置式过滤器只能配参数、逻辑写死;一旦要自定义业务逻辑(鉴权、改请求、记耗时),就得用
GlobalFilter写代码。 GlobalFilter与GatewayFilter在同一条过滤器链里按 order 交错执行,区别在于前者无条件全路由生效且 order 自定,后者靠配置绑定、order 由 Spring 分配。default-filters不是GlobalFilter,它只是作用范围扩大到全部路由的配置式GatewayFilter。- 实现三件套:
implements GlobalFilter写逻辑、@Component注册成 Bean、@Order或Ordered定顺序。 - 两个必背的坑:
chain.filter(exchange)和setComplete()的返回值都必须return,漏了就是请求挂死。 - 网关是 WebFlux 响应式环境,禁引
spring-boot-starter-web,禁写阻塞代码。