一、为什么选择 Spring Cloud Gateway?
在微服务架构中,API 网关是所有外部请求的统一入口,承担着以下职责:
| 职责 | 说明 |
|---|---|
| 路由转发 | 根据请求路径/Header 动态路由到对应服务 |
| 统一鉴权 | JWT 验证、OAuth2 集成 |
| 限流熔断 | 保护下游服务不被流量打垮 |
| 日志追踪 | 统一记录请求链路信息 |
| 跨域处理 | 全局 CORS 配置 |
为什么不用 Zuul?
- Zuul 1.x 基于 Servlet 阻塞 IO,性能瓶颈明显
- Spring Cloud Gateway 基于 Reactor + WebFlux,天然支持响应式、非阻塞
- 官方社区活跃,Spring Boot 3.x / GraalVM 原生支持
二、环境准备
<!-- pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Gateway 核心,内置 WebFlux,不能引入 spring-boot-starter-web -->
<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>
<version>2023.0.1.0</version>
</dependency>
<!-- Redis 响应式客户端(限流使用) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- JWT 鉴权 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
⚠️ 注意:Gateway 基于 WebFlux,项目中不能引入
spring-boot-starter-web,否则启动报错。
三、基础路由配置
3.1 YAML 静态路由
# application.yml
server:
port: 8080
spring:
application:
name: gateway-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
# 开启从注册中心自动路由(lb://serviceName)
discovery:
locator:
enabled: true
lower-case-service-id: true # 服务名小写
routes:
# 路由 1:用户服务
- id: user-service-route
uri: lb://user-service # lb:// 表示负载均衡
predicates:
- Path=/api/user/** # 路径断言
filters:
- StripPrefix=1 # 去掉第一段路径前缀 /api
# 路由 2:订单服务(带版本前缀)
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/api/order/**
- Header=X-Request-Version, v2 # Header 断言
filters:
- StripPrefix=1
- AddRequestHeader=X-Gateway-Source, gateway # 添加请求头
# 路由 3:固定 URL(第三方服务)
- id: external-service-route
uri: https://api.external.com
predicates:
- Path=/ext/**
filters:
- RewritePath=/ext/(?<segment>.*), /$\{segment} # 路径重写
3.2 路由断言(Predicates)常用类型
| 断言 | 示例 | 说明 |
|---|---|---|
| Path | Path=/api/** | 路径匹配 |
| Method | Method=GET,POST | 请求方法 |
| Header | Header=Token, \d+ | Header 正则匹配 |
| Query | Query=userId, \d+ | 查询参数匹配 |
| After | After=2026-01-01T00:00:00+08:00[Asia/Shanghai] | 时间后有效 |
| RemoteAddr | RemoteAddr=192.168.1.0/24 | IP 段白名单 |
四、过滤器链详解
4.1 内置 GatewayFilter
filters:
- StripPrefix=1 # 去除 N 个路径前缀
- AddRequestHeader=X-Trace-Id, #{T(java.util.UUID).randomUUID()}
- AddResponseHeader=X-Frame-Options, DENY
- SetStatus=200 # 强制修改响应状态码
- Retry=3 # 失败重试 3 次
- RequestRateLimiter: # 限流(详见第六节)
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
4.2 全局过滤器(GlobalFilter)— 统一日志
@Component
@Slf4j
public class AccessLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
String method = request.getMethod().name();
String ip = getClientIp(request);
long startTime = System.currentTimeMillis();
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long cost = System.currentTimeMillis() - startTime;
int statusCode = exchange.getResponse().getStatusCode() != null
? exchange.getResponse().getStatusCode().value() : 0;
log.info("[Gateway] {} {} | IP={} | Status={} | Cost={}ms",
method, path, ip, statusCode, cost);
}));
}
private String getClientIp(ServerHttpRequest request) {
String ip = request.getHeaders().getFirst("X-Forwarded-For");
if (ip == null || ip.isBlank()) {
ip = request.getRemoteAddress() != null
? request.getRemoteAddress().getAddress().getHostAddress() : "unknown";
}
return ip;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 1; // 最后执行,保证 cost 准确
}
}
五、JWT 全局鉴权过滤器
5.1 JWT 工具类
@Component
public class JwtUtils {
@Value("${jwt.secret:your-256-bit-secret-key-here-must-be-long-enough}")
private String secretKey;
private SecretKey getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 验证并解析 Token
*/
public Claims validateToken(String token) {
return Jwts.parser()
.verifyWith(getSignKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
public boolean isTokenValid(String token) {
try {
validateToken(token);
return true;
} catch (JwtException e) {
return false;
}
}
}
5.2 鉴权全局过滤器
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private JwtUtils jwtUtils;
/** 白名单路径(不需要鉴权) */
private static final List<String> WHITE_LIST = List.of(
"/api/auth/login",
"/api/auth/register",
"/api/public/",
"/actuator/"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 白名单直接放行
if (isWhiteListed(path)) {
return chain.filter(exchange);
}
// 获取 Token
String token = extractToken(exchange.getRequest());
if (token == null || !jwtUtils.isTokenValid(token)) {
return unauthorized(exchange, "Token 无效或已过期");
}
// Token 合法:将用户信息传递给下游服务
Claims claims = jwtUtils.validateToken(token);
String userId = claims.getSubject();
String roles = claims.get("roles", String.class);
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-User-Id", userId)
.header("X-User-Roles", roles != null ? roles : "")
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
private boolean isWhiteListed(String path) {
return WHITE_LIST.stream().anyMatch(path::startsWith);
}
private String extractToken(ServerHttpRequest request) {
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = """
{"code": 401, "message": "%s"}
""".formatted(message);
DataBuffer buffer = response.bufferFactory()
.wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100; // 高优先级,在限流之前执行
}
}
六、Redis 令牌桶限流
Spring Cloud Gateway 内置了基于 Redis 的令牌桶限流,使用 Lua 脚本保证原子性。
6.1 限流 Key 解析器(按用户 ID)
@Configuration
public class RateLimiterConfig {
/**
* 按用户 ID 限流(鉴权后 Header 中有 X-User-Id)
*/
@Bean
@Primary
public KeyResolver userKeyResolver() {
return exchange -> {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
return Mono.just(userId != null ? userId : "anonymous");
};
}
/**
* 按 IP 限流(兜底策略)
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> {
String ip = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For");
if (ip == null && exchange.getRequest().getRemoteAddress() != null) {
ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
}
return Mono.just(ip != null ? ip : "unknown");
};
}
}
6.2 路由中配置限流
spring:
data:
redis:
host: localhost
port: 6379
cloud:
gateway:
routes:
- id: order-service-rate-limited
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
# 令牌桶每秒补充速率
redis-rate-limiter.replenishRate: 10
# 令牌桶容量(允许突发)
redis-rate-limiter.burstCapacity: 20
# 每次请求消耗令牌数
redis-rate-limiter.requestedTokens: 1
# 使用哪个 KeyResolver Bean
key-resolver: "#{@userKeyResolver}"
6.3 限流响应状态码说明
| 状态码 | 含义 |
|---|---|
| 200 | 正常通过 |
| 429 | Too Many Requests,触发限流 |
可通过自定义过滤器修改限流拦截后的响应体:
@Component
public class RateLimitResponseFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
if (HttpStatus.TOO_MANY_REQUESTS.equals(response.getStatusCode())) {
// 可以在此记录指标、推送告警等
log.warn("[Gateway] Rate limited: {}", exchange.getRequest().getPath());
}
}));
}
@Override
public int getOrder() { return Ordered.LOWEST_PRECEDENCE; }
}
七、全局跨域(CORS)配置
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*"); // 生产环境替换为具体域名
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
⚠️ 注意:Gateway 中配置 CORS 后,下游服务不要再配置 CORS,否则会出现 Header 重复导致浏览器报错。
八、整合 Nacos 动态路由(高级)
静态路由每次修改都需要重启网关,通过 Nacos 配置中心可实现零停机热更新路由。
@Component
@Slf4j
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;
/**
* 更新路由(Nacos 配置变更时调用)
*/
public void updateRoutes(List<RouteDefinition> routeDefinitions) {
// 1. 清除旧路由
clearRoutes();
// 2. 加载新路由
routeDefinitions.forEach(route -> {
routeDefinitionWriter.save(Mono.just(route)).subscribe();
log.info("[Gateway] Dynamic route updated: {}", route.getId());
});
// 3. 发布刷新事件
publisher.publishEvent(new RefreshRoutesEvent(this));
}
private void clearRoutes() {
// 实际项目中需要维护已加载路由 ID 列表,此处简化示意
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
}
Nacos 中存储路由配置格式(JSON):
[
{
"id": "user-service-route",
"uri": "lb://user-service",
"predicates": [
{ "name": "Path", "args": { "pattern": "/api/user/**" } }
],
"filters": [
{ "name": "StripPrefix", "args": { "parts": "1" } }
],
"order": 1
}
]
九、常见问题与踩坑
9.1 引入 spring-boot-starter-web 导致启动失败
报错:Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway
解决:删除 spring-boot-starter-web 依赖,Gateway 已内置 WebFlux。
9.2 使用 LoadBalancer 时找不到服务
报错:503 Service Unavailable - No instances available
排查步骤:
- 确认
spring-cloud-starter-loadbalancer已引入 - 检查 Nacos 服务注册状态
- 确认
uri: lb://service-name中的服务名与 Nacos 注册名一致(注意大小写)
9.3 WebFlux 中不能使用 ThreadLocal
由于响应式编程线程切换,ThreadLocal 传递会丢失,推荐使用 Context:
// 存入 Context
return chain.filter(exchange)
.contextWrite(Context.of("userId", userId));
// 从 Context 取出(在响应式链中)
Mono.deferContextual(ctx -> {
String userId = ctx.get("userId");
// ...
});
9.4 Redis 限流 Lua 脚本报错
报错:NOSCRIPT No matching script
原因:Redis 集群模式下 Lua 脚本 Key 不在同一 slot。
解决:使用 {user}:key 形式的 Hash Tag,确保相关 key 落在同一槽位。
十、生产部署建议
# Dockerfile(结合前几篇 Docker 多阶段构建文章)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/gateway-service.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Xms256m", "-Xmx512m", \
"-Dreactor.netty.http.server.accessLogEnabled=true", \
"-jar", "app.jar"]
性能调优参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
-Xms / -Xmx | 256m / 1g | 根据实际流量调整 |
reactor.netty.ioWorkerCount | CPU 核心数 × 2 | Netty IO 线程数 |
spring.cloud.gateway.httpclient.connect-timeout | 3000 | 下游连接超时(ms) |
spring.cloud.gateway.httpclient.response-timeout | 10s | 下游响应超时 |
十一、总结
本文完整演示了 Spring Cloud Gateway 在 Spring Boot 3.x 微服务架构中的实战应用:
- ✅ 路由配置:静态路由、断言规则、负载均衡
- ✅ 全局过滤器:访问日志、JWT 鉴权、响应修改
- ✅ Redis 限流:令牌桶算法、按用户/IP 维度限流
- ✅ 跨域配置:全局 CORS,避免重复配置
- ✅ 动态路由:结合 Nacos 实现零停机热更新
- ✅ 踩坑总结:WebFlux 常见问题与解决方案
与前几篇文章形成完整的微服务技术栈:
Gateway(本文)→ Sentinel 限流熔断 → Seata 分布式事务 → RocketMQ 异步解耦 → Docker 容器化部署
如有问题欢迎在评论区留言交流 🚀
参考文档:
评论区