楼层: 首页/ 软件技术/ Spring Cloud 微服务/ API 网关:Spring Cloud Gateway
五

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/用户/接口)分桶。