1. 为什么微服务一上量,网关就成了绕不开的那道坎
刚接触 Spring Cloud 那会儿,我总觉得网关是个「可有可无」的组件:服务本来就能互相调,为什么还要在中间横插一层?直到服务从 3 个涨到 12 个,前端同学开始抱怨「接口域名一天变三次」,运维同学发现每个服务都要单独配一遍鉴权和跨域,我才真正理解网关的价值。
Spring Cloud Gateway 是什么?一句话说清:它是 Spring 官方基于 WebFlux 打造的响应式 API 网关,用来做统一入口、路由转发、鉴权、限流和跨域处理。它能做什么?把原本散落在各个微服务里的「公共逻辑」——身份校验、请求头加工、跨域响应头、灰度路由——全部收敛到网关这一层。适合谁?正在从单体拆微服务、或者微服务数量已经超过 5 个、前端开始抱怨接口混乱的后端同学。
和老的 Zuul 比,Gateway 最大的区别是编程模型:Zuul 基于 Servlet,是阻塞式的,一个请求占一个线程;Gateway 基于 Spring 5 的 WebFlux,是响应式的,用少量线程就能扛住高并发。这不是玄学,是实打实的吞吐差异,尤其在网关这种「所有流量必经」的位置上,性能差距会被放大。
这篇文章我会按真实落地顺序讲:先搭一个能跑的最小网关,再把路由断言工厂、局部过滤器、全局过滤器 GlobalFilter、过滤器链执行顺序逐个拆开,最后给出 CORS 跨域配置的可复制 YAML 和 curl 验证。同时结合 TaoToken 的统一 Key/API 通道,演示怎么把网关下游服务的 endpoint 和鉴权配置改到统一通道上,用日志和响应头验证路由命中、过滤器顺序和跨域是否符合预期。
2. 搭建最小可用网关:从依赖到第一条路由
2.1 创建网关模块并引入依赖
新建一个 module,比如叫gateway-demo,pom.xml里引入 Gateway 和 Nacos 服务发现依赖:
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> </dependencies>注意:Gateway 模块不要引入spring-boot-starter-web,否则会和 WebFlux 冲突,启动直接报错。这是新手最容易踩的坑之一。
2.2 启动类与路由配置
启动类很普通:
@SpringBootApplication @EnableDiscoveryClient public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }核心在application.yml。一条路由由四部分组成:id(唯一标识)、uri(目标地址,http是固定地址,lb是按服务名负载均衡)、predicates(断言,判断请求是否符合规则)、filters(过滤器,对请求或响应做加工):
server: port: 10010 spring: application: name: gateway cloud: nacos: server-addr: 127.0.0.1:8848 gateway: routes: - id: user-service uri: lb://userservice predicates: - Path=/user/** - id: order-service uri: lb://orderservice predicates: - Path=/order/**启动后访问http://localhost:10010/user/1,网关会把请求转发到userservice的/user/1。这里lb://userservice的userservice是 Nacos 里注册的服务名,网关会自动做负载均衡。
2.3 把下游 endpoint 与鉴权收敛到 TaoToken 统一通道
真实项目里,下游服务往往要调用大模型 API,如果每个服务各自维护一份 Key,轮换和审计会非常痛苦。我的做法是把网关作为统一出口,下游服务的 endpoint 和鉴权配置改到 TaoToken 的 API 通道上。
TaoToken 的 API 地址是https://taotoken.net/api,官网是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。在网关里加一条路由,把/ai/**的请求转发到统一通道:
- id: taotoken-ai uri: https://taotoken.net/api predicates: - Path=/ai/** filters: - StripPrefix=1 - AddRequestHeader=Authorization, Bearer ${TAOTOKEN_KEY}StripPrefix=1会去掉路径里的第一段/ai,这样/ai/v1/chat/completions转发出去就变成/v1/chat/completions。${TAOTOKEN_KEY}从环境变量读取,避免把 Key 硬编码进配置文件。这样下游服务只需要调网关的/ai/**,Key 的轮换、审计、限流全部在网关这一层完成。
3. 路由断言工厂与过滤器链:可复制配置与执行顺序
3.1 路由断言工厂的匹配规则
配置文件里写的Path=/user/**只是字符串,真正干活的是PathRoutePredicateFactory这个类。Spring 提供了 11 种基础断言工厂,常用的几个:
| 断言工厂 | 作用 | 示例 |
|---|---|---|
| Path | 按路径匹配 | Path=/user/** |
| Method | 按请求方法匹配 | Method=GET,POST |
| Header | 按请求头匹配 | Header=X-Request-Id,\d+ |
| Query | 按查询参数匹配 | Query=token,admin |
| After/Before/Between | 按时间匹配 | After=2024-01-01T00:00:00+08:00 |
| Host | 按域名匹配 | Host=**.example.com |
多个断言之间是「与」关系,必须全部满足才转发。比如同时要求路径是/user/**且方法是 GET:
predicates: - Path=/user/** - Method=GET3.2 局部过滤器与默认过滤器
GatewayFilter是配置式的过滤器,只对当前路由生效。比如给所有进入userservice的请求加一个请求头:
- id: user-service uri: lb://userservice predicates: - Path=/user/** filters: - AddRequestHeader=X-Request-From, gateway如果想让过滤器对所有路由生效,写到default-filters下:
spring: cloud: gateway: default-filters: - AddRequestHeader=X-Global-Header, global-valueSpring 提供了 38 种路由过滤器工厂,覆盖请求头增删改、路径重写、限流、重试等场景,具体可以查官方文档的 GatewayFilter Factories 章节。
3.3 全局过滤器 GlobalFilter 与执行顺序
GlobalFilter和GatewayFilter作用一样,区别在于逻辑要自己写代码。定义一个拦截请求、校验authorization参数的全局过滤器:
@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String auth = exchange.getRequest().getQueryParams().getFirst("authorization"); if ("admin".equals(auth)) { return chain.filter(exchange); } exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } @Override public int getOrder() { return 0; } }过滤器链执行顺序的规则要记牢:每个过滤器都有 int 类型的 order 值,order 越小优先级越高,执行越靠前。GlobalFilter通过实现Ordered接口或加@Order注解指定;路由过滤器和 defaultFilter 的 order 由 Spring 指定,默认按声明顺序从 1 递增。当 order 值相同时,执行顺序是defaultFilter > 路由过滤器 > GlobalFilter。
3.4 CORS 跨域配置的可复制 YAML
跨域的本质是「域名或端口不一致」:localhost:8080和localhost:8081就算跨域。浏览器会拦截跨域 ajax 请求,解决方案是 CORS。网关处理跨域只需一段 YAML:
spring: cloud: gateway: globalcors: add-to-simple-url-handler-mapping: true cors-configurations: '[/**]': allowedOrigins: - "http://localhost:8080" allowedMethods: - GET - POST - PUT - DELETE - OPTIONS allowedHeaders: "*" allowCredentials: true maxAge: 360000add-to-simple-url-handler-mapping: true是为了让 OPTIONS 预检请求也能被处理,否则浏览器预检会 404。maxAge是预检结果缓存时间,单位毫秒。
4. 验证请求:用 curl 和日志确认路由命中与跨域响应头
配置写完不验证等于没写。我习惯用 curl 分三步验证。
第一步,验证路由命中。直接请求网关:
curl -i http://localhost:10010/user/1预期返回200,并且响应体是userservice的数据。如果返回404,说明断言没匹配上,检查Path是否写对、服务是否注册到 Nacos。
第二步,验证过滤器执行顺序。在全局过滤器和局部过滤器里各打一行日志,请求后观察控制台输出顺序。按前面的规则,defaultFilter 先执行,然后是路由过滤器,最后是 GlobalFilter(order 相同时)。如果顺序不对,检查 order 值。
第三步,验证跨域响应头。用 curl 模拟跨域请求,带上Origin头:
curl -i -H "Origin: http://localhost:8080" \ -H "Access-Control-Request-Method: POST" \ -X OPTIONS http://localhost:10010/user/1预期响应头里应该包含:
Access-Control-Allow-Origin: http://localhost:8080 Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS Access-Control-Allow-Credentials: true如果缺少Access-Control-Allow-Origin,说明globalcors配置没生效,检查缩进和add-to-simple-url-handler-mapping是否开启。
第四步,验证 TaoToken 通道。请求网关的/ai/**路由:
curl -i http://localhost:10010/ai/v1/models \ -H "Authorization: Bearer $TAOTOKEN_KEY"如果返回模型列表,说明网关到 TaoToken 的转发链路通了。日志里能看到StripPrefix生效后的真实转发路径。
5. 常见报错排查:401、local proxy failed 与 OAuth 问题
报错一:401 Unauthorized。最常见的原因是 Key 没传或传错。检查AddRequestHeader里的Authorization格式是不是Bearer xxx,以及环境变量TAOTOKEN_KEY是否真的注入到了网关进程。可以在网关里加一行日志打印请求头(注意脱敏),确认 Key 到达了转发层。
报错二:local proxy failed。这个报错通常出现在网关转发到下游时,下游地址不可达。如果是lb://协议,检查服务是否注册到 Nacos、服务名是否拼写一致;如果是https://固定地址,检查网络连通性和 DNS 解析。我遇到过lb://userservice写成lb://user-service导致找不到实例的情况,服务名一定要和 Nacos 里注册的完全一致。
报错三:reading choices 相关错误。这类错误一般出现在调用大模型接口时,响应体解析失败。检查网关是否对响应做了ModifyResponseBody之类的加工,导致 JSON 结构被破坏。如果下游返回的是流式响应(SSE),网关默认会缓冲,需要确认是否配置了spring.cloud.gateway.httpclient.response-timeout等参数。
报错四:OAuth 鉴权失败。如果下游服务用 OAuth,网关转发时可能丢失了Authorization头。Gateway 默认会透传请求头,但如果用了RemoveRequestHeader或SetRequestHeader覆盖,就会丢掉。检查过滤器链里有没有误删鉴权头。
关于 CC Switch / Cline MCP / Codex auth.json 的三件套。如果你在网关下游接了这类编码工具,配置时一定要写全三件套:Base URL、Key、Model ID。Base URL 填 TaoToken 的 API 地址https://taotoken.net/api,Key 填你的 TaoToken Key,Model ID 填具体模型名。缺任何一个都会导致鉴权或路由失败。
6. 把网关接入 TaoToken:统一 Key 与 API 通道的落地建议
网关这一层跑通之后,下一步是把下游服务的鉴权配置统一收敛。我的建议是:所有需要调用大模型的下游服务,都不直接持有 Key,而是通过网关的/ai/**路由转发。
具体做法是在网关里配置一条带鉴权的路由,下游服务只调网关地址。这样 Key 只在网关一处维护,轮换时改一个环境变量即可。TaoToken 的 API 地址是https://taotoken.net/api,接入文档在https://taotoken.net/doc,API Keys 管理在https://taotoken.net/api-keys。如果你需要长期跑编码 Agent 或做批量调用,可以看看 Coding Plan:https://taotoken.net/coding-plan。想先验证模型效果,直接用模型对话页面:https://taotoken.net/chat。
实测下来,把网关作为统一出口之后,最明显的变化是排障变简单了:以前一个 401 要在五六个服务里翻日志,现在只看网关一处就能定位是 Key 问题还是路由问题。另一个好处是限流和审计可以集中做,不用每个服务重复实现。
最后提醒一句:网关的default-filters不要塞太多逻辑,尤其是涉及响应体加工的过滤器,容易和流式响应冲突。把重逻辑放到 GlobalFilter 里用代码控制,配置层保持轻量,后期维护会轻松很多。