一、网关选型的血泪史
2019年,公司微服务架构刚刚拆分完成。10个微服务,每个都有自己的Controller,每个都暴露在公网。
噩梦开始:
- 前端同学要记住10个服务地址
- 每次服务上线都要通知前端改配置
- 没有统一的安全策略,各服务各自为战
- 日志分散,查一个问题要翻10个服务的日志
我开始调研API网关,试过Nginx、Kong、Zuul,最后选择了Spring Cloud Gateway。
今天把踩过的坑和积累的经验分享出来。
二、为什么需要API网关?
2.1 没有网关的痛
┌────────────────────────────────────────┐
│ 前端应用 │
└────────────────┬───────────────────────┘
│
┌────────┼────────┬────────┬───────┐
↓ ↓ ↓ ↓ ↓
┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐
│用户服务 ││订单服务 ││商品服务 ││支付服务 ││库存服务 │
│:8081 ││:8082 ││:8083 ││:8084 ││:8085 │
└────────┘└────────┘└────────┘└────────┘└────────┘
问题:
1. 前端需要知道所有服务地址
2. 无法统一认证和安全
3. 无法统一日志和监控
4. CORS问题严重
2.2 有网关的好处
┌────────────────────────────────────────┐
│ 前端应用 │
└────────────────┬───────────────────────┘
│
┌───────┴───────┐
│ API Gateway │
│ :8080 │
│ │
│ - 统一入口 │
│ - 认证鉴权 │
│ - 限流熔断 │
│ - 日志监控 │
│ - 路由转发 │
└───────┬───────┘
│
┌────────┼────────┬────────┬───────┐
↓ ↓ ↓ ↓ ↓
┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐
│用户服务 ││订单服务 ││商品服务 ││支付服务 ││库存服务 │
└────────┘└────────┘└────────┘└────────┘└────────┘
好处:
1. 前端只需记住网关地址
2. 统一认证,安全可控
3. 统一日志和监控
4. CORS一次配置
三、Spring Cloud Gateway核心概念
3.1 工作原理
┌─────────────────────────────────────────────────────────────┐
│ Spring Cloud Gateway │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Gateway Handler Mapping │ │
│ │ - 匹配路由 │ │
│ │ - RouteDefinitionLocator │ │
│ │ - RoutePredicateHandlerMapping │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Gateway Web Handler │ │
│ │ - Filter Chain 执行 │ │
│ │ - 顺序执行 Pre Filter │ │
│ │ - 调用下游服务 │ │
│ │ - 顺序执行 Post Filter │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
请求流程:
1. 客户端请求到达 Gateway
2. HandlerMapping 匹配路由规则
3. WebHandler 构建 Filter Chain
4. Pre Filter 依次执行(认证、日志、限流...)
5. 调用下游服务
6. Post Filter 逆序执行(响应处理、日志...)
7. 返回客户端
3.2 三大核心概念
java
// 1. Route(路由)
// 路由 = 断言 + 过滤器 + 目标URI
Route route = Route.builder()
.id("order-service") // 路由ID
.uri("lb://order-service") // 目标服务(lb表示负载均衡)
.predicate(PathRoutePredicateFactory.class, "/order/**") // 断言
.filter(FilterA.class) // 过滤器A
.filter(FilterB.class) // 过滤器B
.build();
// 2. Predicate(断言)
// 用于匹配HTTP请求的各种条件
// - Path:路径匹配
// - Method:请求方法匹配
// - Header:请求头匹配
// - Query:查询参数匹配
// - Cookie:Cookie匹配
// - RemoteAddr:远程地址匹配
// 3. Filter(过滤器)
// 在请求前后做处理
// - Pre Filter:请求转发前执行(认证、日志、限流)
// - Post Filter:响应返回前执行(响应处理、日志)
四、快速入门:第一个Gateway应用
4.1 引入依赖
xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 服务发现支持 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
4.2 配置文件
yaml
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
# 路由配置
routes:
- id: order-service
uri: lb://order-service # lb表示负载均衡
predicates:
- Path=/order/**
filters:
- StripPrefix=1 # 去掉第一层路径
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- StripPrefix=1
- id: product-service
uri: lb://product-service
predicates:
- Path=/product/**
filters:
- StripPrefix=1
# 默认过滤器(对所有路由生效)
default-filters:
- DedupeResponseHeader=Vary Access-Control-Allow-Origin
# 日志级别
logging:
level:
org.springframework.cloud.gateway: DEBUG
4.3 测试验证
bash
# 启动网关后,测试路由转发
curl http://localhost:8080/order/create
# 会被转发到 order-service/create
curl http://localhost:8080/user/info
# 会被转发到 user-service/info
五、路由配置详解
5.1 基础路由配置
yaml
spring:
cloud:
gateway:
routes:
# 基础路由
- id: static-route
uri: http://example.com
predicates:
- Path=/static/**
filters:
- RewritePath=/static(?<segment>/?.*), $\{segment}
# 带权重的路由
- id: order-v1
uri: http://order-v1.example.com
predicates:
- Path=/order/**
- Weight=80 # 80%流量
- id: order-v2
uri: http://order-v2.example.com
predicates:
- Path=/order/**
- Weight=20 # 20%流量
5.2 动态路由(服务发现)
yaml
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 开启服务发现路由
lower-case-service-id: true # 服务ID小写
# 开启后,可以通过以下方式访问:
# http://gateway:8080/order-service/order/create
# 自动转发到 order-service 的 /order/create 接口
# 或者简化路径:
# http://gateway:8080/order-service/order/create
# 如果只配置了 lb://order-service
5.3 路由断言工厂
yaml
spring:
cloud:
gateway:
routes:
- id: complex-predicates
uri: lb://user-service
predicates:
# 1. Path 路径匹配
- Path=/user/**
# 2. Method 请求方法
- Method=GET,POST
# 3. Header 请求头
- Header=X-Request-Id, \d+
# 4. Query 查询参数
- Query=name, zhang.*
# 5. Cookie
- Cookie=session, abc.*
# 6. RemoteAddr 远程地址
- RemoteAddr=192.168.1.0/24
# 7. After 时间之后
- After=2024-01-01T00:00:00+08:00[Asia/Shanghai]
# 8. Before 时间之前
- Before=2024-12-31T23:59:59+08:00[Asia/Shanghai]
# 9. Between 某段时间内
- Between=2024-01-01T00:00:00+08:00[Asia/Shanghai], \
2024-12-31T23:59:59+08:00[Asia/Shanghai]
六、过滤器实战
6.1 内置过滤器
yaml
spring:
cloud:
gateway:
routes:
- id: with-filters
uri: lb://order-service
predicates:
- Path=/order/**
filters:
# 1. AddRequestHeader:添加请求头
- AddRequestHeader=X-Gateway, gateway
# 2. AddRequestHeadersIfNotPresent:添加请求头(不存在时)
- AddRequestHeadersIfNotPresent=X-Request-Id,123
# 3. RemoveRequestHeader:移除请求头
- RemoveRequestHeader=X-Internal-Header
# 4. AddResponseHeader:添加响应头
- AddResponseHeader=X-Gateway, gateway
# 5. RemoveResponseHeader:移除响应头
- RemoveResponseHeader=Server
# 6. RewritePath:重写路径
- RewritePath=/order(?<segment>/?.*), $\{segment}
# 7. SetPath:设置路径
- SetPath=/api${segment}
# 8. StripPrefix:去掉路径前缀
- StripPrefix=1
# 9. SetStatus:设置状态码
- SetStatus=401
# 10. RedirectTo:重定向
- RedirectTo=302, https://example.com
6.2 自定义全局过滤器
java
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private JwtUtil jwtUtil;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 跳过登录和注册接口
if (path.startsWith("/auth/login") || path.startsWith("/auth/register")) {
return chain.filter(exchange);
}
// 获取Token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(token)) {
log.warn("请求缺少Token: path={}", path);
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 去掉Bearer前缀
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
// 验证Token
try {
Claims claims = jwtUtil.parseToken(token);
String userId = claims.getSubject();
// 把用户ID放入请求属性
ServerWebExchange modifiedExchange = exchange.mutate()
.request(builder -> builder.header("X-User-Id", userId))
.build();
return chain.filter(modifiedExchange);
} catch (Exception e) {
log.warn("Token验证失败: {}", e.getMessage());
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
@Override
public int getOrder() {
return -100; // 优先级,数字越小越先执行
}
}
6.3 自定义路由过滤器
java
@Component
@Slf4j
public class RequestLogFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 记录请求开始时间
long startTime = System.currentTimeMillis();
String requestId = UUID.randomUUID().toString();
// 打印请求日志
log.info("请求开始: method={}, path={}, requestId={}",
request.getMethod(),
request.getPath(),
requestId);
// 在响应完成后打印日志
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
log.info("请求结束: method={}, path={}, status={}, duration={}ms, requestId={}",
request.getMethod(),
request.getPath(),
exchange.getResponse().getStatusCode(),
duration,
requestId);
}));
}
@Override
public int getOrder() {
return 0;
}
}
// 在路由中引用
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder,
RequestLogFilter requestLogFilter) {
return builder.routes()
.route("order-service", r -> r
.path("/order/**")
.filters(f -> f
.filter(requestLogFilter) // 添加自定义过滤器
.addRequestHeader("X-Gateway", "gateway")
)
.uri("lb://order-service"))
.build();
}
七、限流与熔断
7.1 基于Redis的限流
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
yaml
spring:
cloud:
gateway:
routes:
- id: rate-limit-route
uri: lb://order-service
predicates:
- Path=/order/**
filters:
# 基于Redis的令牌桶限流
# 100个请求/秒,每个IP最多10个/秒
- name: RequestRateLimiter
args:
redis-token-bucket:
capacity: 100 # 令牌桶容量
refill-rate: 100 # 每秒补充令牌数
key-resolver: "#{@ipKeyResolver}" # 按IP限流
redis-token-bucket-per-user:
capacity: 10
refill-rate: 10
key-resolver: "#{@userKeyResolver}"
java
@Configuration
public class RateLimitConfig {
/**
* 按IP限流
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress() != null
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
: "unknown"
);
}
/**
* 按用户限流
*/
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
return Mono.just(userId != null ? userId : "anonymous");
};
}
/**
* 按API限流
*/
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getPath().value()
);
}
}
7.2 熔断配置
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-spring-cloud-gateway</artifactId>
</dependency>
yaml
spring:
cloud:
gateway:
routes:
- id: circuit-breaker-route
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- name: CircuitBreaker
args:
name: orderCircuitBreaker
fallbackUri: forward:/fallback/order # 熔断时的降级URI
java
@RestController
@RequestMapping("/fallback")
public class FallbackController {
@GetMapping("/order")
public Result<?> orderFallback(ServerWebExchange exchange) {
String path = exchange.getRequest().getPath().value();
log.warn("Order服务熔断,降级处理: path={}", path);
return Result.fail(503, "服务暂时不可用,请稍后再试");
}
@GetMapping("/user")
public Result<?> userFallback(ServerWebExchange exchange) {
return Result.fail(503, "用户服务暂时不可用");
}
}
八、统一认证与鉴权
8.1 JWT认证过滤器
java
@Component
@Slf4j
public class JwtAuthFilter implements GlobalFilter {
@Value("${jwt.secret:default-secret}")
private String secret;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 白名单路径跳过认证
if (isWhiteList(path)) {
return chain.filter(exchange);
}
String token = getToken(exchange);
if (token == null) {
return unauthorized(exchange, "未提供认证令牌");
}
try {
Claims claims = parseToken(token);
String userId = claims.getSubject();
String roles = claims.get("roles", String.class);
// 验证权限
if (!hasPermission(path, roles)) {
return forbidden(exchange, "权限不足");
}
// 添加用户信息到请求头
ServerWebExchange modified = exchange.mutate()
.request(builder -> builder
.header("X-User-Id", userId)
.header("X-User-Roles", roles))
.build();
return chain.filter(modified);
} catch (Exception e) {
log.warn("JWT验证失败: {}", e.getMessage());
return unauthorized(exchange, "令牌无效或已过期");
}
}
private boolean isWhiteList(String path) {
return path.startsWith("/auth/")
|| path.startsWith("/public/")
|| path.equals("/health");
}
private boolean hasPermission(String path, String roles) {
// 管理员可以访问所有接口
if ("ADMIN".equals(roles)) {
return true;
}
// 其他角色按需校验
if (path.startsWith("/admin/") && !"ADMIN".equals(roles)) {
return false;
}
return true;
}
private String getToken(ServerWebExchange exchange) {
String auth = exchange.getRequest().getHeaders().getFirst("Authorization");
if (auth != null && auth.startsWith("Bearer ")) {
return auth.substring(7);
}
return null;
}
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret.getBytes())
.parseClaimsJws(token)
.getBody();
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders().add("Content-Type", "application/json");
String body = "{\"code\":401,\"message\":\"" + message + "\"}";
return exchange.getResponse().writeWith(
Mono.just(exchange.getResponse().bufferFactory().wrap(body.getBytes()))
);
}
private Mono<Void> forbidden(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
exchange.getResponse().getHeaders().add("Content-Type", "application/json");
String body = "{\"code\":403,\"message\":\"" + message + "\"}";
return exchange.getResponse().writeWith(
Mono.just(exchange.getResponse().bufferFactory().wrap(body.getBytes()))
);
}
}
8.2 动态权限配置
yaml
spring:
cloud:
gateway:
routes:
# 公开接口
- id: public-routes
uri: lb://public-service
predicates:
- Path=/public/**
# 用户接口(需登录)
- id: user-routes
uri: lb://user-service
predicates:
- Path=/user/**
# 管理员接口(需ADMIN角色)
- id: admin-routes
uri: lb://admin-service
predicates:
- Path=/admin/**
九、CORS跨域配置
java
@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);
}
}
十、踩坑实录
坑1:路由顺序问题
有一天,一个路由规则不生效。排查半天发现是路由顺序的问题。
原因:Gateway按配置顺序匹配路由,第一个匹配的路由会生效。
解决:把更精确的路由放在前面,更通用的放在后面。
yaml
# 错误顺序:/user/detail会先匹配到/user/**
routes:
- id: user-list
uri: lb://user-service
predicates:
- Path=/user/**
- id: user-detail # 永远不会被匹配到
uri: lb://user-service
predicates:
- Path=/user/detail/**
# 正确顺序:
routes:
- id: user-detail # 精确路由放前面
uri: lb://user-service
predicates:
- Path=/user/detail/**
- id: user-list
uri: lb://user-service
predicates:
- Path=/user/**
坑2:Path匹配问题
配置了
Path=/order/**,但访问/order//create时报404。原因 :
//会被认为是路径的一部分。解决 :在路由过滤器中添加
RewritePath,规范化路径。
yaml
filters:
- RewritePath=/order/(?<segment>.*), /$\{segment}
坑3:服务发现路由大小写问题
开启了服务发现路由,但有些服务能访问,有些不能。
原因:服务ID大小写不一致。
解决:开启小写转换。
yaml
spring:
cloud:
gateway:
discovery:
locator:
lower-case-service-id: true # 服务ID转小写
坑4:全局过滤器优先级问题
我的认证过滤器明明先执行,但Token还没验证就被路由出去了。
原因:过滤器优先级设置不对。
解决 :使用
Ordered接口设置优先级,数字越小越先执行。认证过滤器建议设置为-100到-50之间。
十一、总结
Spring Cloud Gateway是微服务架构的统一入口:
- 路由转发:把请求分发到对应的微服务
- 断言匹配:灵活匹配HTTP请求的各种条件
- 过滤器链:在请求前后做统一处理
- 限流熔断:保护后端服务不被击垮
- 统一认证:在网关层做认证鉴权
最佳实践:
- 路由规则按从精确到粗粒度排序
- 认证过滤器优先级要高于其他过滤器
- 限流要结合Redis等外部存储
- 熔断降级要做好,给用户友好提示
- 日志要打全,方便排查问题
血的教训:
网关是所有流量的入口,一旦出问题影响巨大。上线前一定要做好充分的测试,特别是限流和熔断功能。不要以为上了网关就万事大吉,要持续监控网关的各项指标。
思考题: 你的项目用的是什么网关?有没有遇到过路由匹配的问题?
个人观点,仅供参考