五
API 网关:Spring Cloud Gateway
Spring Cloud Gateway
第三个灵魂问题:前端要调 10 个服务,难道让前端记住 10 个地址?不能。所有请求必须从一个大门进来,这个大门就是 API 网关。它管三件事:路由(请求该去哪个服务)、过滤(鉴权、限流、改 header)、统一入口(前端只认识网关一个地址)。
论网关三件套:路由、断言、过滤器
Route(路由):一条规则 = "什么样的请求 + 转发到哪个地址"。比如 /api/user/** 转发到 user-service。
Predicate(断言):判断条件。路径匹配?Method 是 GET?Header 里有某个 token?满足条件才走这条路由。
Filter(过滤器):在转发前后干的活。鉴权检查、限流计数、加请求头、改响应。全局过滤器对所有路由生效。
Spring Cloud Gateway 基于 WebFlux(异步非阻塞),比老一代 Zuul 快。注意它跑在 Netty 上,不要在网关里引 spring-boot-starter-web,否则启动报错。
YAML 方式定义路由(最常用)
pom.xml 加网关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 网关也要从 Nacos 拿服务地址 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
application.yml:把 /api/user/** 路由到 user-service,/api/order/** 路由到 order-service
server:
port: 9000 # 网关自己的端口,前端就认这个
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: user-route # 路由名字,随便起,唯一就行
uri: lb://user-service # lb:// 表示走负载均衡,从 Nacos 找服务
predicates:
- Path=/api/user/** # 断言:路径匹配 /api/user/ 开头就走这条
filters:
- StripPrefix=1 # 剥掉第一段 /api,转发给 user-service 时变成 /user/xxx
- id: order-route
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
跨域 CORS 与全局过滤器
前端浏览器跨端口调接口会被 CORS 拦。网关里统一配一下,就不用每个服务各配一遍了。鉴权也是——在网关统一校验 token,后面的服务就不用重复查了。
网关统一配置跨域(application.yml)
spring:
cloud:
gateway:
globalcors:
cors-configurations:
# 对所有路由生效
'[/**]':
allowedOriginPatterns: "*" # 生产环境写具体域名,别用 *
allowedMethods: ["GET", "POST", "PUT", "DELETE"]
allowedHeaders: "*"
allowCredentials: true
写一个全局过滤器:检查请求头里有没有 token
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;
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从请求头里拿 Authorization
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || token.isBlank()) {
// 没带 token,直接返回 401,不往下走
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 校验通过,放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100; // 数字越小越先执行
}
}
与 Nacos 集成:路由配置动态化
路由写在 application.yml 里,改一条路由就得重启网关。生产环境更灵活的做法是把路由规则存到 Nacos,网关监听 Nacos 变化,热更新路由。思路是:网关实现一个 RouteDefinitionRepository,从 Nacos 读路由 JSON,Nacos 配置一变就调 RouteDefinitionWriter 刷新。网上有现成封装,搜 gateway nacos dynamic route,以官方生态最新实现为准。
网关限流:RequestRateLimiter
网关是最适合限流的位置——所有请求都过它。Spring Cloud Gateway 自带 RequestRateLimiter 过滤器,底层用 Redis 令牌桶算法。想按用户、按 IP 限流,自定义一个 KeyResolver 就行。Sentinel 也能做网关限流,功能更全(控制台配规则),生产环境常用 Sentinel + Gateway。
完整案例:网关路由 + 全局过滤器 + 限流 + 测试
把前面讲的串成一个能跑的网关:路由两条业务线、写一个鉴权全局过滤器、用 RequestRateLimiter 做令牌桶限流、配好跨域。
第一步:加限流依赖(RequestRateLimiter 底层要 Redis)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
第二步:application.yml 完整路由 + 限流 + 跨域
server:
port: 9000
spring:
cloud:
gateway:
routes:
- id: user-route
uri: lb://user-service
predicates: [ Path=/api/user/** ]
filters:
- StripPrefix=1
# 路由级限流:每秒 10 个请求
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
- id: order-route
uri: lb://order-service
predicates: [ Path=/api/order/** ]
filters: [ StripPrefix=1 ]
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods: ["GET","POST"]
allowedHeaders: "*"
第三步:自定义 KeyResolver,按请求 IP 限流
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}
// 配了这个 Bean,RequestRateLimiter 就按 IP 分别限流
第四步:起 Redis 后测试
# 正常转发:网关收到 /api/user/1 → 剥成 /user/1 → 转发 user-service
curl http://localhost:9000/api/user/1 -H "Authorization: Bearer abc"
# 没带 token → 全局过滤器 AuthFilter 返回 401
curl http://localhost:9000/api/user/1
# < HTTP/1.1 401 Unauthorized
# 一秒内猛刷 30 次 → 超过 burstCapacity=20 的请求被限流
# < HTTP/1.1 429 Too Many Requests
网关路由不生效,先查这三点
① uri 写的是 lb://service-name,你得确保 Nacos 里真有这个服务名,别拼错。② StripPrefix 数不对——你写了 StripPrefix=1,转发时会剥掉第一段路径,下游服务如果没有对应路径就 404。③ 引了 spring-boot-starter-web,Gateway 是 WebFlux 跑在 Netty 上,混了 Tomcat starter 直接起不来。
本章面试题
面试快答 · API 网关
Q1. Gateway 为什么比 Zuul 快?为什么不能引 spring-boot-starter-web?
参考答案
Gateway 基于 Spring WebFlux + Netty,异步非阻塞,一个线程处理多路连接;Zuul 1.x 是同步阻塞,每请求一线程。引了 starter-web(Tomcat Servlet 容器)会跟 WebFlux 的 Netty 启动方式冲突,直接报"failed to determine WebApplicationType"。
Q2. Route / Predicate / Filter 三者关系?
参考答案
Route = 一条完整规则(id + uri + predicates + filters);Predicate 是"这个请求匹不匹配这条路由"的判断;Filter 是"匹配后转发前后要干的活"。路由是容器,断言是入口,过滤器是加工。
Q3. 网关限流用什么算法?
参考答案
RequestRateLimiter 默认令牌桶(Token Bucket):按 replenishRate 匀速补令牌,burstCapacity 允许瞬间突发。用 Redis 存计数实现分布式限流,KeyResolver 决定按什么维度(IP/用户/接口)分桶。