当微服务从三五个膨胀到几十个,客户端如果直连每个服务,地址暴露、跨域、重复鉴权、限流分散等麻烦会接踵而至。Spring Cloud Gateway 是 Spring 官方推出的 API 网关,基于 WebFlux 响应式内核,把路由转发、统一鉴权、限流熔断和日志观测收敛到微服务唯一入口。本文从零搭建一个生产可用的网关,给出可直接复制的路由、基于 Redis 的限流、JWT 鉴权与熔断降级配置。
一、为什么需要 API 网关
没有网关时,前端要记住 user-service、order-service、pay-service 各自的地址与端口,一旦服务扩容或迁移,所有调用方都要改。网关把这些细节屏蔽掉,只暴露一个域名,内部拓扑对调用方完全透明。举个具体例子:前端原本要写死 http://10.0.1.12:8081/api/user 和 http://10.0.2.7:8082/api/order,某天 order-service 从 8082 迁到 8090,前端发版才能跟上;有了网关后,前端永远只调 https://api.example.com,迁移对调用方零感知。
1.1 网关必须兜住的四件事
- 路由:按路径、域名、请求头把流量转发到正确服务。
- 鉴权:在入口统一校验令牌,避免每个服务重复写登录逻辑。
- 限流:保护后端不被突发流量打垮。
- 可观测:集中记录访问日志、埋点链路。
相比老牌的 Zuul 1.x(基于 Servlet 阻塞模型),Spring Cloud Gateway 走的是 WebFlux + Reactor 响应式栈,连接数上限更高、延迟更低,也是目前 Spring 官方主推的方案。下面用一张表对比三种常见选型:
| 方案 | 编程模型 | 限流能力 | 适用场景 |
|---|---|---|---|
| Zuul 1.x | Servlet 阻塞 | 需自研 | 老 Spring Cloud 项目 |
| Spring Cloud Gateway | WebFlux 响应式 | 内置 RequestRateLimiter | 新微服务、云原生 |
| Nginx | 事件驱动 | 漏桶/limit_req | 纯转发、静态路由 |
二、核心模型:Route、Predicate、Filter
Gateway 的每次转发都由一条 Route(路由) 描述,路由由三部分组成:断言 Predicate 决定请求是否匹配,过滤器 Filter 在转发前后改写请求与响应。一个路由典型形如:id、uri(目标服务地址)、predicates(匹配规则)、filters(处理逻辑)。Predicate 支持 Path、Method、Header、Query、Host、Cookie、时间区间等多种匹配,还能用 and / or 组合,足以覆盖绝大多数路由规则。
2.1 最小依赖与配置
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
注意:网关本身不需要 spring-boot-starter-web,引入它会因为内嵌 Tomcat 与 WebFlux 冲突导致启动失败。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: user_route
uri: lb://user-service
predicates:
- Path=/api/user/**
三、路由配置实战
3.1 基于 Path 的路由
最常见的是按路径前缀转发,lb:// 表示走服务发现(需配合 Nacos 或 Eureka),把 /api/order/** 转发到 order-service。
- id: order_route
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
StripPrefix=1 会去掉第一层路径 /api,后端收到的就是 /order/**,避免路径污染。
3.2 用 Java DSL 写动态路由
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("pay_route", r -> r.path("/api/pay/**")
.filters(f -> f.stripPrefix(1)
.rewritePath("/pay/(?<id>.*)", "/v1/${id}"))
.uri("lb://pay-service"))
.build();
}
YAML 适合静态规则,Java DSL 适合需要根据配置中心动态生成路由的场景。
3.3 权重路由做灰度发布
上线新版本最怕“一把梭”全量切换。Gateway 内置 Weight 断言,可以把一部分流量按比例引到新版本,验证无误再逐步放量。
spring:
cloud:
gateway:
routes:
- id: order_v1
uri: lb://order-service-v1
predicates:
- Path=/api/order/**
- Weight=orderGroup, 90
- id: order_v2
uri: lb://order-service-v2
predicates:
- Path=/api/order/**
- Weight=orderGroup, 10
上面的配置把 order 服务的 10% 流量打到 v2,其余 90% 仍走稳定的 v1。注意两个路由必须属于同一个 Weight group,否则比例不生效。
四、限流:基于 Redis 的令牌桶
网关层限流最稳妥的做法是基于 Redis 的令牌桶算法。Gateway 内置 RequestRateLimiter 过滤器,配合 Redis 实现集群级统一计数,多个网关节点共享同一份计数,不会出现“每台各自限流、整体超限”的漏洞。
spring:
cloud:
gateway:
routes:
- id: user_route
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getQueryParams().getFirst("userId"));
}
replenishRate 是每秒补充的令牌数,burstCapacity 是桶容量,超过就会返回 429。生产环境务必按用户、IP 或接口维度做 key,避免误伤正常流量。若网关与 Redis 都跑在容器中,建议同内网部署以降低限流判断的网络开销(可参考 Docker 编排实践 与 Redis 缓存设计)。
五、鉴权:JWT 全局过滤器
把登录校验收敛到网关,后端服务只认“已通过网关”的请求。下面用 GlobalFilter 实现 JWT 校验,失败时直接返回 401,请求根本不会转发到下游。
@Component
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtUtil jwtUtil;
private static final List<String> WHITE = List.of("/api/auth/login");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (WHITE.contains(path)) return chain.filter(exchange);
String token = resolveToken(exchange.getRequest());
if (token == null || !jwtUtil.validate(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override public int getOrder() { return -1; }
}
白名单放行登录接口,其余请求必须携带合法 JWT。注意过滤器顺序 getOrder() 要排在限流之前,先鉴权再限流更合理:没登录的请求没必要占用限流配额。
六、熔断与降级
单个下游服务超时会拖垮整个网关线程池。借助 Resilience4j 集成,可以为路由加上超时、熔断与降级响应。
spring:
cloud:
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- name: CircuitBreaker
args:
name: orderCircuit
fallbackUri: forward:/fallback/order
当 order-service 错误率超阈值,网关直接走 /fallback/order 返回降级数据,保护核心链路。超时配置要略小于下游服务自身超时,避免“上游已断、下游还在跑”。
七、鉴权之后:把用户身份透传给下游
网关校验完 JWT 后,下游服务往往还需要知道“当前是谁在访问”。与其让每个服务各自解析 JWT,不如在网关层把解析出的用户 ID、角色写进请求头,再转发给下游,下游直接读头即可,既省算力又统一了身份语义。
@Bean
public GlobalFilter userHeaderFilter(JwtUtil jwtUtil) {
return (exchange, chain) -> {
String token = resolveToken(exchange.getRequest());
Claims claims = jwtUtil.parse(token);
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Roles", claims.get("roles", String.class))
.build();
return chain.filter(exchange.mutate().request(mutated).build());
};
}
这里要特别小心:下游服务必须信任“只有网关能写入这些头”,否则客户端可以伪造 X-User-Id 越权。常见做法是网关在入口剥离外部传入的 X-User-* 头,只由网关内部写入,并在网络层保证下游只接收来自网关的流量。
八、生产落地清单与避坑
| 检查项 | 建议 |
|---|---|
| 跨域 CORS | 网关统一配置,不要每个服务各配一套 |
| 超时对齐 | 网关与下游超时一致,避免级联等待 |
| 健康检查 | 暴露 /actuator/health 并接入监控 |
| 链路日志 | 记录 traceId,便于全链路排查 |
| TLS 终结 | 网关终结 HTTPS,内网走 HTTP,前置 ” + link(261,”Nginx 反向代理”) + ” 更稳 |
常见坑:① 引入 web 依赖导致 WebFlux 冲突;② 限流 key 太粗(全站一个 key)把正常用户一起限掉;③ 过滤器顺序错乱导致未鉴权就放行;④ 网关本身没做健康检查,扩缩容时流量倾斜;⑤ 透传身份头却没在入口剥离外部伪造头,埋下越权隐患。
把网关纳入 GitHub Actions 流水线 与容器化发布,结合 微服务治理 与 微服务七大陷阱 的踩坑经验,能少走很多弯路。基于 Spring Boot 3 的项目请记得使用 Jakarta 命名空间。
九、网关与 Nginx 如何分工
很多人会问:已经有了 Nginx 反向代理,为什么还要网关?一句话:Nginx 擅长“四层/七层纯转发与静态规则”,而 Gateway 擅长“和业务语义绑定的动态路由、鉴权与限流”。典型分工是 Nginx 在最外层做 TLS 终结、域名虚拟主机和粗粒度限流,Gateway 在内部做按路径的细粒度路由、JWT 校验和可编程过滤。两者不是替代关系,而是“外层硬转发 + 内层软治理”的互补。
十、小结
Spring Cloud Gateway 用一套“路由 + 过滤器”的声明式模型,把微服务入口的复杂度压到了最低。路由解决“去哪”,JWT 过滤器解决“能不能进”,Redis 限流解决“别被打垮”,Resilience4j 解决“挂了怎么办”,权重路由解决“怎么灰度”。先把这一套跑通,再逐步叠加全链路追踪与配置中心动态路由,你的微服务入口就稳了。




