CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
Spring Cloud Gateway 路由、过滤器、统一鉴权与限流入口。
Spring Cloud Gateway
这一课你将能做到
独立搭起 Gateway,把 /api/users/** 路由到 user-service、/api/problems/** 路由到 problem-service
写一个自定义 GlobalFilter 统一校验登录态,并向下游注入用户身份 Header
给对外接口配置基于 IP 的限流,说清网关过滤器链的执行顺序
为什么要学
微服务一拆多,前端就要面对 user-service:8081、problem-service:8082、judge-service:8083 一堆地址,每个服务还得各写一遍 CORS、JWT 校验、限流——重复且容易漏。API 网关 把这些横切关注点收口到唯一入口:前端只认一个域名,内部怎么拆、怎么扩容前端完全无感。
本站单机版是 Nginx 把 /api 反代到 Spring Boot:8080;Gateway 就是应用层的 Nginx ,但它跑在 Java 里、认识注册中心,能用代码写路由与鉴权逻辑。生产常见组合是「Nginx 终止 TLS + Gateway 做应用层路由」,两者不是二选一。
先建立图像
一次请求穿过网关的路径:
浏览器 GET /api/problems/1
│
▼
Gateway:8080
│ ① 路由匹配(Predicate):Path=/api/problems/** 命中 problem 路由
│ ② 过滤器链(Filter Chain)pre 部分:鉴权 → 加 Header → 限流
│ ③ 转发:lb://problem-service → LoadBalancer 选实例 → http://10.0.0.7:8082/problems/1
│ ④ 过滤器链 post 部分:改响应、记日志
▼
problem-service 返回 JSON
两个核心概念:
Predicate(断言) :决定「这个请求归哪条路由」,如 Path、Method、Header、Time。
Filter(过滤器) :决定「转发前后做点什么」,与 Servlet Filter 思想一致,但基于 WebFlux。
跟着做
步骤 1:创建 gateway 模块
pom.xml(注意:Gateway 基于 WebFlux,绝对不能再引入 spring-boot-starter-web ,否则启动直接冲突报错):
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>api-gateway</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
</project>
启动类就是普通 Spring Boot 启动类,无特殊注解:
package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
步骤 2:配置路由 server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: user-route
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- id: problem-route
uri: lb://problem-service
predicates:
- Path=/api/problems/**
filters:
- StripPrefix=1
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
lb://user-service:lb 即 load balance——从 Eureka 按 user-service 这个名字查实例列表并负载均衡,这就是网关和注册中心的联动。
Path=/api/users/**:路径断言,命中才走这条路由。
StripPrefix=1:转发前去掉路径第一段,/api/users/1 变成 /users/1,恰好对上 user-service 自己的 @RequestMapping("/users")。
启动 eureka-server、user-service、problem-service、api-gateway 四个进程后访问:
curl http://localhost:8080/api/users/1 # → user-service 响应
curl http://localhost:8080/api/problems/1 # → problem-service 响应
步骤 3:常用内置过滤器 spring:
cloud:
gateway:
default-filters: # 对所有路由生效
- AddRequestHeader=X-Trace-Source, gateway
routes:
- id: problem-route
uri: lb://problem-service
predicates:
- Path=/api/problems/**
filters:
- StripPrefix=1
- Retry=3 # 网络异常自动重试 3 次(仅幂等 GET 场景)
- AddResponseHeader=X-Response-By, gateway
全局 CORS 也建议在网关统一配,业务服务不再各自配置:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "https://www.codeforge.example"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true
步骤 4:自定义 GlobalFilter 统一鉴权 package com.example.gateway.filter;
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
/** 白名单:登录、注册不校验 */
private static final List<String> WHITE_LIST =
List.of("/api/auth/login", "/api/auth/register");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // pre 阶段直接短路返回
}
// 真实项目在此解析并校验 JWT 签名,取出 userId 后注入 Header 传给下游
String userId = "1001"; // demo:模拟解析结果
var mutated = exchange.getRequest().mutate()
.header("X-User-Id", userId)
.build();
return chain.filter(exchange.mutate().request(mutated).build());
}
@Override
public int getOrder() {
return -100; // 数值越小越先执行,鉴权放最前面
}
}
下游服务从 X-User-Id Header 取用户身份,不必再解析 JWT——鉴权做一次,全站受益 。
步骤 5:基于 Redis 的 IP 限流 加依赖 spring-boot-starter-data-redis-reactive,配置:
spring:
data:
redis:
host: localhost
port: 6379
cloud:
gateway:
routes:
- id: problem-route
uri: lb://problem-service
predicates:
- Path=/api/problems/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒放行 10 个
redis-rate-limiter.burstCapacity: 20 # 突发容量 20
key-resolver: "#{@ipKeyResolver}"
package com.example.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
@Configuration
public class RateLimitConfig {
/** 按「IP+路径」限流:同一 IP 打不同接口互不影响 */
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
+ ":" + exchange.getRequest().getPath().value());
}
}
超限请求会收到 429 Too Many Requests。底层是 Redis + Lua 实现的令牌桶算法 ,分布式环境下所有网关实例共享同一个计数器。
讲清楚要点
路由三要素 :id(唯一标识)、uri(转发目标,lb://服务名 或 http://固定地址)、predicates(匹配条件)。
过滤器链有 pre/post 两个阶段 :请求先按 order 从小到大走完所有 pre 逻辑 → 转发到下游 → 再按相反顺序走 post 逻辑;chain.filter() 之后的代码就是 post 阶段。
GlobalFilter 与 GatewayFilter :Global 作用于所有路由,GatewayFilter 绑定单条路由(yml filters: 配的多数是这类)。
lb:// 的本质 :网关内置 LoadBalancer,按服务名查注册中心拿实例列表,默认轮询选一个发请求——注册中心挂了它还能靠本地缓存转发。
Gateway 是响应式(WebFlux)非阻塞模型 :少量线程即可扛高并发转发,代价是不能引入 spring-boot-starter-web ,也别在过滤器里写阻塞调用(如同步 JDBC)。
与 Nginx 分工 :Nginx 管 TLS、静态资源、粗粒度负载均衡;Gateway 管动态路由、业务级鉴权限流。两层并存是主流。
你可能会懵的地方
为什么 Gateway 模块不能加 spring-boot-starter-web?
web 是 Servlet(Tomcat 阻塞模型),Gateway 是 WebFlux(Netty 非阻塞模型),两者都要占据应用主类型,共存时启动即报 Spring MVC found on classpath 错误。网关里需要调 HTTP 就用 WebClient/RestClient。
多个过滤器 order 相同怎么办?
按注册顺序执行,行为不保证稳定。显式实现 Ordered 接口或用 @Order 注解定死顺序:鉴权 -100 → 限流 -50 → 日志 0,一目了然。
StripPrefix 什么时候用、什么时候不用?
用不用取决于下游服务的真实路径。下游是 @RequestMapping("/users")、对外约定 /api/users,就 Strip 掉 1 段;如果下游直接写 @RequestMapping("/api/users"),就不 Strip。团队统一一种约定最好。
网关挂了是不是全站挂了?
是单点,所以要部署多实例 + 前面挂 Nginx/LB 做高可用;路由规则存在网关自己内存里,实例之间无状态,随便横向加。
路由可以不写在 yml 里吗?
可以。实现 RouteLocator Bean 用 Java DSL 写,或接 Nacos/数据库做动态路由(改配置实时生效)。小项目 yml 足够,别一开始就上动态路由。
错法对照 错法 问题 正解 Gateway 依赖里加了 spring-boot-starter-web Servlet 与 WebFlux 冲突,启动失败 只留 gateway 相关 starter,HTTP 调用用 WebClient uri 写成 http://localhost:8081 绕过注册中心,扩容即失效 lb://user-service + 服务名在 GlobalFilter 里写同步 JDBC 查权限 阻塞 Netty 事件线程,吞吐暴跌 过滤器内只做轻量校验,或全链路响应式 网关和业务服务都配 CORS 响应头重复,浏览器报错 只在网关统一配置,服务端删除 对非幂等 POST 配 Retry=3 重复提交,扣款/判题重复执行 重试只给幂等请求;写操作用幂等键
卡点急救
启动报 Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway :依赖冲突,删掉 spring-boot-starter-web/spring-boot-starter-tomcat,只保留 spring-cloud-starter-gateway。
转发报 503 Service Unavailable :目标服务没注册上 Eureka 或缺 loadbalancer 依赖。确认服务在 Eureka 控制台是 UP,且网关 pom 里有 spring-cloud-starter-loadbalancer。
限流永远不生效 / 一直 429 :检查 Redis 连通性、key-resolver 的 Bean 名与 yml 里 #{@ipKeyResolver} 是否一致;本地调试可把 replenishRate 临时调大。
面试常问
讲讲 Gateway 过滤器链的执行流程。
请求进入 → 按路由规则匹配(Predicate)→ 收集该路由的 GatewayFilter + 所有 GlobalFilter,按 order 排序 → 依次执行 pre 逻辑 → 转发下游 → 拿到响应后按逆序执行 post 逻辑。类比 Servlet Filter 责任链,但基于 Reactor 非阻塞。能画出「鉴权 pre → 转发 → 日志 post」示意即达标。
网关限流怎么实现?
内置 RequestRateLimiter 过滤器 + Redis 令牌桶:以 KeyResolver 决定限流维度(IP/用户/接口),Lua 脚本在 Redis 原子地「放令牌-扣令牌」,多实例共享计数。参数:replenishRate(每秒生成令牌数)、burstCapacity(桶容量)。答出「为什么用 Lua:保证判断与扣减的原子性」是加分点。
Gateway 和 Nginx 怎么分工?
Nginx:L7 反代、TLS 终止、静态资源、网关集群的负载均衡;Gateway:认识注册中心做 lb:// 动态路由、Java 代码级鉴权限流灰度。典型链路:客户端 → Nginx → Gateway 集群 → 微服务。
自测三问
Predicate 和 Filter 各管什么?GlobalFilter 与 GatewayFilter 区别?
lb://problem-service 从转发到发出请求,中间发生了什么?
为什么网关的过滤器里不能写阻塞 JDBC 调用?
小结一句话 Gateway 是微服务统一前门:断言决定「谁处理」,过滤器决定「怎么处理」,lb:// 联动注册中心实现动态路由,鉴权限流跨域都在门口做一次。
延伸