☰
Spring Cloud Gateway实战:路由、断言、过滤器与跨域配置全解析
2026/10/18 13:45:55 网站建设 项目流程

微服务做久了,几乎谁都绕不开这两个词:Spring Boot 和 Spring Cloud。我经常在面试里碰到候选人张口就能背“Spring Boot是基础框架,Spring Cloud是微服务全家桶”,但真让他落地一个服务网关的时候,却连路由怎么写、依赖怎么引、请求到网关之后为什么会 404 都说不清楚。这篇文章就以服务网关为核心线索,把这两个框架的区别与联系摊开讲清楚,然后从零搭一个基于 Spring Cloud Gateway 的网关服务,把路由、断言、过滤器、跨域这些内容一次说明白,最后再附上生产环境里我踩过的一些坑。适合正在做微服务改造、或者刚接手网关模块的开发者参考。

1. Spring Boot 和 Spring Cloud:先分清地基和楼层

1.1 Spring Boot 解决的是“单个服务怎么开发”

Spring Boot 解决的问题,说白了就是让开发者用一个内嵌容器、一堆 starter 依赖和少量注解,快速启动一个可用的服务。

想象一下没有 Spring Boot 的年代,一个普通的 Spring MVC 项目要配置多少东西:web.xml、Spring 容器、数据源、事务管理器、视图解析器……每一项都需要 XML 配置文件,项目一多,光复制粘贴配置就能把人写崩溃。Spring Boot 用“约定大于配置”的方式把这些全部接管了。你只需要引入spring-boot-starter-web,写一个带@RestController的类,加上@SpringBootApplication注解,main 方法里一跑,一个 Tomcat 服务就起来了。

放到微服务语境里,Spring Boot 就是每个独立服务的底座。订单服务、商品服务、用户服务,各自用 Spring Boot 做开发,各自独立部署、独立扩展,这是微服务拆分的基础姿态。但问题也随之而来:服务拆开了,谁来管它们之间的协作?服务之间怎么互相发现?配置怎么统一管理?某个服务挂了怎么熔断?外部请求应该从哪里统一进来?这些都不归 Spring Boot 管。

1.2 Spring Cloud 解决的是“多个服务怎么协同”

Spring Cloud 不是一个具体框架,而是一组分布式系统解决方案的集合。它构建在 Spring Boot 之上,用一套统一编程模型把服务注册发现、配置中心、负载均衡、熔断降级、服务网关、分布式链路追踪这些东西打包在一起。

举个例子,服务 A 要调用服务 B,在 Spring Cloud 体系里,流程是这样的:服务 B 启动时把自己注册到注册中心(比如 Nacos 或 Eureka),服务 A 通过负载均衡组件从注册中心拿到服务 B 的实例列表,再选择其中一台发起调用。如果服务 B 挂了,注册中心会自动摘掉它的节点,服务 A 的调用会自动跑到还活着的节点上。这套逻辑完全透明,服务 A 的代码里只需要写一个带@FeignClient的接口,剩下的交给 Spring Cloud 处理。

所以说,Spring Boot 关心的是“单体服务内部怎么组织”,Spring Cloud 关心的是“分布式环境下的服务治理”。两者不是平行的东西,Spring Cloud 是站在 Spring Boot 肩膀上发展出来的生态层,没有 Spring Boot 的自动配置和条件装配能力,Spring Cloud 的很多组件根本没法玩得这么转。

1.3 版本兼容性:最容易踩坑的地方

因为 Spring Cloud 依赖 Spring Boot 的自动装配机制,所以它对 Spring Boot 版本有严格的对应关系。很多刚接触的人从网上随便找了个依赖版本,结果一启动就报一堆莫名其妙的ClassNotFoundException或者IllegalArgumentException,大概率就是版本对不上。

Spring Cloud 版本命名有个特点,小版本用伦敦地铁站名,比如 Angel、Brixton、Camden、Dalston、Edgware、Finchley、Greenwich、Hoxton,再往后直接用年份,比如 2020.0.x。每个版本对应了特定的 Spring Boot 大版本,必须严格匹配。

我建议用这套组合:Spring Boot 2.3.x 对应 Spring Cloud Hoxton.SR12,Spring Cloud Gateway 对应 2.2.x 版本。这是生产环境中已经足够成熟、被大量项目验证过的组合。虽然现在 Spring Boot 3.x 也出来了,但考虑到不少公司还在用 JDK 8,很多老项目不会轻易迈过这道坎。

Spring Boot 版本Spring Cloud 版本备注
2.2.xHoxton.SR6 或更早老项目常见
2.3.xHoxton.SR8 以后稳定性好,推荐
2.4.x 以后2020.0.x依赖导入方式有变化
3.0.x2022.0.x要求 JDK 17

依赖弄好之后,可以用 Maven 的依赖树命令检查是否引入了冲突的传递依赖,比如确认没有把spring-boot-starter-web误带进来:

mvn dependency:tree -Dincludes=org.springframework.boot:spring-boot-starter-web

网关工程如果出现这个依赖,十有八九会启动失败,后面我会专门讲。

2. 服务网关在微服务里的真实位置

2.1 没有网关的时候,系统有多乱

我接过一个模拟商城系统的改造项目,原来的架构是典型的单体服务直接对外暴露接口。客户端要查订单、查商品、查优惠券,就得分别记住三个服务域名,比如order.example.com、product.example.com、coupon.example.com。每个服务的鉴权逻辑各自写一遍,有的服务用 JWT,有的服务用 Session,有的服务压根没有鉴权,直接在接口里裸奔。

结果就是:前端要维护一堆服务地址,后端每个服务都有一堆重复的过滤器代码,加一个限流功能就得改所有服务,排查线上问题时日志散布在各自的机器上。最要命的是安全,网关没有统一收口,内部接口全都暴露在外网,攻击面被拉得很大。

后来引入服务网关之后,客户端只认一个域名api.example.com,后端服务全部改成内网地址,所有请求先打到网关,由网关根据路径把请求转发到对应服务。前端既不用关心服务拆成了多少个,也不用知道服务的真实部署位置,安全性和可维护性都上来了。

2.2 网关的四大核心职责

服务网关在微服务架构里承担的核心职责,可以归纳成四件事。

第一是路由转发。客户端请求进来之后,网关要根据请求的路径、Header、参数等条件,找到匹配的后端服务。比如/api/order/**开头的请求要转发到订单服务,/api/product/**开头的请求要转发到商品服务。这是网关最基础的功能,没有路由,网关就只是个空壳。

第二是集中过滤。鉴权、日志、跨域、灰度发布这种横切逻辑,全部收敛到网关这一层做。订单服务只需要关心订单本身的业务,不需要在每个 Controller 里重复写用户校验。

第三是流量控制。网关可以在入口处做限流,比如某个接口每秒只允许 100 个请求,超出的直接返回繁忙提示。这样即使某个后端服务被突发流量打崩,网关也能先挡住一波,给后端争取伸缩容的时间。

第四是协议转换。客户端对外统一走 HTTP/HTTPS,网关内部再决定是转发给 HTTP 服务还是 WebSocket 服务,甚至在复杂点架构里还可以转换成 RPC 协议。这一层对客户端完全透明。

2.3 网关和注册中心是怎么配合的

网关能实现动态路由,核心在于它和注册中心打通了。网关启动时把注册中心的地址配置好,注册中心上所有服务实例信息就会被网关感知到。配置路由的时候,不需要写死目标服务的 IP,而是写服务名,网关从注册中心拿到这个服务名对应的所有实例列表,做负载均衡之后选择一台转发。

这个机制带来的好处非常明显:服务实例扩容、缩容、迁移,只要注册中心的数据更新了,网关的路由行为就自动跟着更新,不需要人工改网关配置。后面实操部分我用lb://order-service这种写法,就是从注册中心按服务名做负载均衡的意思,不直接写 IP。

3. 动手实践:基于 Spring Cloud Gateway 实现服务网关

3.1 为什么选 Spring Cloud Gateway,而不是 Zuul 1.x

很多老项目还在用 Zuul 1.x,但我不推荐在新建项目里继续用它。Zuul 1.x 基于 Servlet 和 Tomcat,是阻塞式 I/O 模型,一个请求占据一个线程,高并发下线程池很快会被打满。Spring Cloud Gateway 基于 Spring WebFlux 和 Netty,是非阻塞式的,事件驱动,在高并发、长连接场景下的表现明显更好。

另外 Spring Cloud Gateway 的路由断言和过滤器模型设计得更灵活,配置也更集中,配合 Nacos 或者 Consul 做动态刷新都很方便。所以在 2024 年的今天,如果没有历史包袱,直接选 Spring Cloud Gateway 就对了。

3.2 创建网关工程和依赖引入

先建一个普通的 Maven 工程,只留一个空壳。核心依赖就两组,一组是 Gateway 本身,另一组是 Nacos 注册中心客户端。我用 Nacos 做服务注册发现,所以引入的 Spring Cloud Alibaba 依赖版本要和 Spring Cloud Hoxton 匹配。

<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> <version>2.2.6.RELEASE</version> </dependency>

这里有一个新手必踩的坑:千万不要在网关工程里引入spring-boot-starter-web。Spring Cloud Gateway 是建立在 WebFlux 之上的,如果你把 Spring MVC 也带进来,启动时会直接报错,提示Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway。我见过不止一个同事,从订单服务里把 pom 复制过来改,结果把spring-boot-starter-web也带进来了,一跑就挂。

启动类就是一个普通的 Spring Boot 应用,不需要额外加什么特殊注解:

@SpringBootApplication public class GatewayServerApplication { public static void main(String[] args) { SpringApplication.run(GatewayServerApplication.class, args); } }

3.3 路由配置:从固定地址到服务发现

先把基础的 application.yml 写上。我这里假设有两个后端服务,一个叫order-service,一个叫product-service,都已经注册到了 Nacos 上。

server: port: 9000 spring: application: name: gateway-server cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id: order-route uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=2 - id: product-route uri: lb://product-service predicates: - Path=/api/product/** filters: - StripPrefix=2

配置拆开来看:

uri: lb://order-service里的lb是 load balancer 的缩写,意思是让网关从注册中心拉取order-service的所有实例,然后做客户端负载均衡。如果不写lb,直接写http://127.0.0.1:8080,那网关就不会走注册中心,每次只转发到固定地址,这种叫静态路由。

predicates是断言,也就是路由匹配条件。Path=/api/order/**表示路径以/api/order/开头的请求匹配这条路由。断言不只有 Path 一种,后面我再展开讲。

StripPrefix=2是过滤器,作用是把路径的前两段去掉。比如客户端访问/api/order/detail/123,转发给后端服务时路径会变成/detail/123。为什么要这么做?因为网关是对外统一入口,后端服务内部的接口路径可能没有/api/order这层前缀,如果不去掉,后端服务会 404。

3.4 断言的其他玩法

使用 Path 断言是最常见的,但在实际生产里,其他几种断言也经常能派上用场:

  • Method 断言:只匹配特定的 HTTP 方法,比如Method=GET,POST。
  • Header 断言:匹配请求头,比如Header=X-Request-Id, \d+,只有请求头里的 X-Request-Id 是数字时才匹配。
  • Query 断言:匹配 URL 参数,比如Query=page,要求请求里必须有 page 参数。
  • Host 断言:匹配域名,比如Host=**.example.com。
  • After / Before / Between 断言:匹配时间窗口,可以用来做定时开放的接口,比如秒杀活动时段限定。

把这些断言组合起来,可以写出非常精细的路由规则。例如参加活动的接口可以只在活动时间段生效:

- id: seckill-route uri: lb://seckill-service predicates: - Path=/api/seckill/** - Method=POST - Between=2024-01-01T00:00:00+08:00, 2024-06-30T23:59:59+08:00

这里务必注意:新的 Spring Cloud Gateway 版本里,Between参数支持两个时间字符串,时间格式是 ISO 8601,如果格式不对,路由直接无法启动。

3.5 自定义全局过滤器:实现一个简单鉴权

路由解决了请求去哪里的问题,过滤器则解决了请求怎么去的问题。Spring Cloud Gateway 的过滤器分两种:一种是GatewayFilter,配合某个路由使用,只在特定路由下生效;另一种是GlobalFilter,对所有请求生效。

生产环境里,统一鉴权通常用GlobalFilter来做。我写一个非常精简的版本,思路是:从 Header 里取 token,没有 token 直接返回 401,有 token 就通过修改请求头的方式,把用户标识传给后端服务。这个做法在真实项目里很常见,后端服务就不用自己解析 token 了。

@Component public class AuthFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String token = request.getHeaders().getFirst("Authorization"); if (token == null || token.isEmpty()) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 实际项目中这里会调用鉴权服务校验token,甚至解析出userId和角色权限 // 这里只做演示,直接写死一个用户标识 ServerHttpRequest mutatedRequest = request.mutate() .header("X-User-Id", "1001") .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } @Override public int getOrder() { return -100; } }

getOrder()方法返回的值决定了过滤器的执行顺序,值越小越先执行。这里返回 -100,表示让它非常早地运行。如果项目里有多个全局过滤器,比如一个是鉴权过滤器,一个是日志过滤器,一个是跨域过滤器,那么顺序一定要设计好。日志过滤器应该放在最外层,这样它记录的请求信息最完整;鉴权过滤器在日志之后、业务转发之前。

特别提醒,自定义过滤器里千万不要写Thread.sleep()这种同步阻塞代码。Gateway 底层是 Netty 事件循环,线程数非常有限,一旦有同步阻塞,整个网关的吞吐量都会崩掉。真要做异步操作,必须走Mono或Flux方式,或者把操作委托给独立的线程池。

3.6 跨域配置:前后端分离绕不开的事

浏览器跨域是前端和网关之间必须处理的一环。后端服务在网关后面,所有请求都从同一个域名进入,所以在网关层统一配置跨域就可以了,不需要每个后端服务都写@CrossOrigin。

spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowedOrigins: "*" allowedMethods: "*" allowedHeaders: "*" allowCredentials: true

这里有一个安全提醒:allowedOrigins在生产环境不要写*,最好指定前端域名白名单。如果开了allowCredentials: true,那么allowedOrigins就不能是*了,这是浏览器的 CORS 规范强制要求的,写成*会被部分浏览器拒绝。

我之前就遇到过这样一个事故:开发阶段图省事,网关的跨域配置全部用*,上生产之后前端突然调不通接口,排查了很久才发现是浏览器对allowCredentials和通配符组合的限制。这个坑不算深,但非常容易被忽略。

4. 常见问题与排查技巧实录

4.1 网关路由不生效,一直 404 或 503

这是新手遇到最多的问题。如果你访问网关的时候报 404,先不要急着改代码,按下面这个顺序排查:

第一,确认目标服务是否真的注册到了注册中心。可以登录 Nacos 控制台,在服务列表里搜索order-service。如果服务不在这里,说明服务端注册有问题,网关再怎么配都找不到目标。

第二,确认路由的断言是否匹配你实际访问的路径。用 curl 直接打网关地址,观察日志里有没有打印匹配到哪条路由。比如:

curl -i http://localhost:9000/api/order/detail/123

第三,查看路由里是否写了StripPrefix。如果没写,后端服务收到的路径会带/api/order前缀,而真实接口里没有这段路径,结果就是 404。

第四,检查uri是否写的是lb://服务名,服务名是否和注册中心里的名字完全一致。服务名大小写不一致、多了后缀、写错字,都会导致找不到可用实例。

如果错误信息是No provider available for lb://order-service,那基本可以断定注册中心里没有对应服务,或者网关没有正确从注册中心拿到实例列表。

4.2 依赖冲突导致启动失败

启动时报下面的错误,十有八九是依赖问题:

Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway.

Spring Cloud Gateway 需要 WebFlux 环境,而spring-boot-starter-web引入的是 Spring MVC 和 Tomcat,二者不能共存。解决办法是去掉spring-boot-starter-web依赖,然后检查有没有其他依赖间接带入了它。有些通用 starter 会把 web 依赖带进来,所以最好用mvn dependency:tree查一下。

4.3 过滤器顺序导致的鉴权绕过

接上面说的getOrder(),我见过有人把鉴权过滤器的 order 设置成正数,而某个日志过滤器是负数,日志过滤器率先执行了转发逻辑。这在某些极端情况下会造成安全问题。

以我自己的习惯,统一鉴权过滤器的 order 固定在 -200 到 -100 这个区间,日志过滤器在 -100 到 0 之间,跨域处理器放在最后。这样无论怎么加过滤器,鉴权都会先于日志和转发执行。项目里如果过滤器比较多,建议把 order 的分配规则写在设计文档里,别让后来的人随便改。

4.4 路由配置的刷新问题

Spring Cloud Gateway 的路由配置写在 yml 里的时候,是启动时加载的静态配置。一旦改了路由,必须重启网关才能生效。但在生产环境下,路由可能随时要调整,比如临时下线一台服务、给某个接口加断言,总不能每次都重启网关。

解决办法是引入动态路由能力,把路由定义存储到 Nacos 配置中心或者 Redis 里,然后实现RouteDefinitionRepository接口,通过配置中心的变更通知来刷新路由缓存。网关上可以用ApplicationEventPublisher发布RefreshRoutesEvent,把内存中的路由重新加载一遍。这样配置中心一改,网关无需重启就能生效。

4.5 日志排查技巧

排查网关问题,第一件事就是看访问日志。Spring Cloud Gateway 自带的日志默认比较粗糙,建议自己写一个日志过滤器,把请求路径、Method、响应状态码、耗时、目标服务 ID 全部打出来。我一般只打一行,便于日志平台检索:

[GATEWAY] method=GET path=/api/order/detail/123 status=200 target=order-service cost=45ms

这样出了问题,直接按 traceId 或者路径关键字搜日志,很快就能定位到是网关层的问题还是后端服务的问题。

4.6 网关的高可用部署

很多人把网关当单体应用来部署,一台机器挂了,整个入口就断了。网关本身是无状态的,它不保存业务数据,所以完全可以横向多实例部署,前面再挂一层负载均衡器对外提供服务。Nginx 或者云上负载均衡都可以,把请求平均分发给多个网关实例。这样单个网关崩溃,其他实例照样能把流量接管过去。

5. 生产环境下的三条底线和两个我踩过的坑

5.1 三条底线

第一,网关不要写业务逻辑。网关层应该只做路由、鉴权、日志、限流这些通用横切关注点,什么订单金额计算、库存扣减这种业务逻辑千万不能出现在网关里。业务逻辑写在网关里,既拖慢转发效率,又让网关变得难以维护,时间久了迟早出问题。

第二,限流一定要早做。很多项目上线初期没有限流,说是等后面再加,结果第一次做活动引流,后端服务直接被打挂。网关层用 Sentinel 做限流很方便,后续有时间可以专门讲讲 Sentinal 的网关适配。性能指标那一栏里,限流是性价比最高的一项投入。

第三,访问日志必须保留。网关是流量的必经之地,所有请求都会从这里经过,这是天然的全链路日志入口。把访问日志接入日志平台,按天归档,遇到问题复盘的时候,这里是最可靠的原始证据。

5.2 我踩过的两个坑

第一个坑是网关自己也注册到了 Nacos,并且路由规则写得太宽,结果外部请求打到网关上,网关按路由配置一匹配,发现目标服务名就是自己,于是开始自己转发给自己,直接搞成循环调用。排查了半天才发现是路由规则里/api/**把网关自己的名字也匹配进去了。后来我养成了一个习惯:网关本身不会把自己注册到注册中心,除非有服务需要显式调用网关。

第二个坑是跨域配置里allowCredentials和allowedOrigins的组合问题。当时我把跨域配成allowedOrigins: "*",又在头信息里带了 Cookie,结果浏览器直接拦截响应。翻了浏览器控制台才知道,allowCredentials: true的时候服务端不能返回通配符域名。把allowedOrigins改成具体的前端域名之后,问题才解决。

5.3 什么时候可以不引入网关

也不是所有系统都必须上网关。如果项目只有一个服务,没有服务拆分,外部请求直接打这个服务就行,这时候上网关只是多了一层转发,纯粹增加复杂度。另外如果团队已经在用 Kubernetes Ingress 或者 Service Mesh 方案,外部流量的入口已经被接管了,那 Spring Cloud Gateway 也不是唯一选项。不过对于大多数 Spring 体系内的微服务项目来说,Spring Cloud Gateway 依然是轻量、可控、与现有技术栈打通成本最低的选择。

最后说点个人的体会。我最早学 Spring Cloud 的时候,总想着把所有组件一下子都铺起来,结果每个组件都是一知半解,出了问题根本不知道怎么查。后来才意识到,理解 Spring Boot 和 Spring Cloud 的边界,比背一堆注解和配置重要得多。服务网关更是这样,它的代码量通常不大,核心价值在于能不能把“统一流量入口”这件事真正做好,路由规则、过滤器顺序、限流阈值、日志规范,每一项都需要认真设计。我的建议是先拿一个小 Demo 把路由和全局过滤器跑通,再去碰动态路由和限流熔断,踩坑成本会低很多。趁着现在网关相关的开源生态已经非常成熟,花一个周末把这件事做扎实,以后做微服务改造的时候会轻松不少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询