Java框架 SpringCloud 快速入门: 全局过滤器(GlobalFilter)自定义鉴权

概述

前面用 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 重写请求路径

它们有两个共同点,也正是局限所在:

  1. 只能配参数,不能写逻辑。工厂背后的处理代码是 Spring 提前写死的,你在 yml 里能改的只有传进去的参数值(比如头名叫什么、值是多少)。
  2. 配置在哪一级,作用范围就锁死在哪一级 。写在某个 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&lt;Void&gt; 表示当前过滤器的业务处理完成
     */
    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,禁写阻塞代码。
相关推荐
白山编程大哥1 小时前
C语言篇:可变参数函数
c语言·开发语言
(Charon)1 小时前
【C++面试】程序崩溃如何定位:从Segmentation fault到GDB、Core Dump与ASan
开发语言·c++
paopaokaka_luck1 小时前
高校社团管理(AI辅助任务分配、协同过滤算法推荐、ECharts数据可视化、活动参与闭环、校园交流与反馈、器材借还管理)
java·前端·javascript·spring boot·数据分析·echarts
“AI国潮设计-小江”1 小时前
《Python+SDXL实战:用ControlNet精准控制“英歌舞戚风蛋糕”质感,附批量生成脚本》
开发语言·人工智能·python·prompt·aigc
1024奇点1 小时前
【仓颉语言入门 · 第26课】
开发语言·ide·开源
jayhgq1 小时前
新一代Python包与项目管理工具——UV
开发语言·python·uv
xixiaoyunya1 小时前
CPU 100% 本身不是问题,它是一个症状
开发语言·php
vx_Biye_Design1 小时前
springboot小区管理系统68491-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express
weixin_440401691 小时前
质朴的可视化绘图+pyecharts
开发语言·python·信息可视化·pyecharts