主题
API 网关设计:路由、鉴权与插件化
本文是微服务系统学习系列的 L2 核心篇。前置:28. 注册发现与配置中心、30. 限流熔断与降级。 学完可以配合面试题食用:03-gateway-zuul-kong-route-rate-limit-circuit-break-auth、26-multi-layer-rate-limiting
网关是什么,为什么需要它
微服务架构里,每个服务都暴露自己的端口和地址。客户端直接调用多个服务会面临三个问题:每个服务都要独立鉴权,混在一起很难统一做限流,前端还得知道所有服务的地址。网关就是在客户端和服务之间插一层,把所有横切关注点(鉴权、路由、限流、日志)集中到这一层处理。
关键边界:网关只做通信治理,不做业务逻辑。如果你在网关里写订单校验、库存查询,那网关就变成了一个单体应用——回到了当初要拆微服务的起点。
三种实现路线
业界主流有三条路,选型取决于团队技术栈和运维能力:
Spring Cloud Gateway(SCG):Java 生态的首选。基于 Spring WebFlux(Reactor + Netty),非阻塞 IO,天然适合网关这种 IO 密集场景。Filter 链模型让扩展很直接,写个 GlobalFilter 就能插入自定义逻辑。
Kong / OpenResty:基于 Nginx + Lua,性能极高,单机可扛数万 QPS。插件生态丰富(限流、鉴权、日志都有现成的),通过 Lua 插件扩展。适合已经用 Nginx 做反向代理的团队,或者对 Java 不感冒的团队。
Envoy:C++ 实现,用 xDS 协议做动态配置,与 Istio 等 Service Mesh 集成。不依赖任何语言,进程外架构,对业务完全透明。但运维复杂度高,适合有 Mesh 基础设施的团队。
选型建议:Java 团队无脑 SCG,非 Java 团队看运维能力——Kong 适合运维弱的,Envoy 适合运维强且要走 Mesh 的。
SCG 核心原理:Reactor + Filter 链
SCG 基于 Spring WebFlux,底层是 Reactor 的 Flux/Mono 响应式流。为什么不用 Servlet?因为网关的每个请求要经过多个过滤器(鉴权、限流、路由转发),传统 Servlet 的同步模型会阻塞线程。WebFlux 用少量线程处理大量请求,只有当请求真正到达后端服务时才占用线程,其他时候(比如等待鉴权结果)线程可以处理其他请求。
SCG 的请求处理分两步:路由匹配和过滤器执行。
路由匹配靠 Predicate 工厂。每个路由定义一组 Predicate(Path、Header、Query 等),SCG 按顺序匹配,命中第一个就执行该路由的过滤器链。Predicate 是组合的——可以同时按 Path 和 Header 匹配,来做灰度路由。
过滤器分两种:GlobalFilter(全局执行,所有路由通用)和 GatewayFilter(路由级别的,只对特定路由生效)。一个请求进来,先走 GlobalFilter 的 pre 逻辑,再走 GatewayFilter 的 pre 逻辑,然后发请求到后端,响应回来再走 filter 的 post 逻辑。
java
// 一个 GlobalFilter 的执行顺序:pre->发请求->post
// 多个 Filter 通过 order 排序,值越小越靠前
public interface GatewayFilter extends ShortcutConfigurable {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}鉴权落地:JWT 在网关,授权在服务
常见的坑:每个服务自己校验 JWT Token。这样每个服务都要维护一份公钥,改签名算法时到处改,而且 Token 过期前没法在网关层拦截。
正确做法:网关做 JWT 验签和用户身份解析,服务做细粒度授权。
网关收到请求后,从 Authorization 头取 JWT,验签通过后把用户信息(userId、roles、tenantId)写到请求头里透传给下游服务。下游服务只从请求头取用户身份,不需要自己解 JWT。
java
@Component
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {
private final SecretKey secretKey = Keys.hmacShaKeyFor("my-256-bit-secret-key-for-jwt-signature".getBytes());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
String token = authHeader.substring(7);
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
// 把用户信息透传到下游
ServerWebExchange mutatedExchange = exchange.mutate()
.request(r -> r.header("X-User-Id", claims.getSubject())
.header("X-User-Roles", claims.get("roles", String.class)))
.build();
return chain.filter(mutatedExchange);
} catch (JwtException e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
@Override
public int getOrder() {
return -100; // 高优先级,最先执行
}
}下游服务不需要任何 JWT 依赖,直接读 X-User-Id 头做鉴权:
java
@GetMapping("/orders")
public List<Order> listOrders(@RequestHeader("X-User-Id") String userId) {
// 按 userId 查订单,不需要再解 Token
return orderService.findByUserId(userId);
}这样设计的好处:密钥只存在网关,改签名算法只需要改网关一处;拦截无效 Token 的成本在网关层,后端服务不承受无谓的 JWT 解析开销;新增下游服务时不需要配 JWT。
路由配置与灰度发布
SCG 的路由配置可以写在 yaml 里,也可以从 Nacos 动态拉取。
yaml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service # 通过注册中心负载均衡
predicates:
- Path=/api/orders/**
- Weight=order-group, 90 # 90% 流量走稳定版
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
key-resolver: "#{@userKeyResolver}"
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
- id: order-service-canary
uri: lb://order-service-canary
predicates:
- Path=/api/orders/**
- Header=X-Canary, true # 携带灰度头才走这个路由
filters:
- StripPrefix=1灰度发布的实现思路:一个请求进来,先经过 GlobalFilter 检查用户是否在灰度白名单(按 userId 取模、按 IP 段、或者按 Cookie),如果在白名单就注入 X-Canary: true 头,路由匹配到灰度版;否则走稳定版。
性能与高可用
网关本身不能成为单点。一个网关实例挂掉,整个系统不可用——所以网关必须多实例部署,前置负载均衡器(Nginx / SLB)。
两个关键配置:
连接池与超时:
ConnectTimeout和ReadTimeout都要设,防止后端一个慢服务拖死网关线程。SCG 的 WebFlux 虽然不阻塞,但慢请求会占用连接池连接,池满了照样拒绝新请求。背压与慢请求隔离:用
HystrixGatewayFilterFactory或CircuitBreakerGatewayFilterFactory给每个路由单独设置熔断阈值。订单服务慢到 5 秒就熔断,不影响用户服务。网关层面的熔断保护的是"网关自身"——如果后端整体响应变慢,网关的响应也变慢,最终会拖垮 Nginx 的连接池,所以熔断在网关层是最后一道防线。
yaml
filters:
- name: CircuitBreaker
args:
name: orderServiceBreaker
fallbackUri: forward:/fallback/orders
statusCodes:
- 500
- 504常见误区与小结
- 误区 1:把业务逻辑写进网关。网关的职责是"转发",不是"处理"。路由里写 if-else 判断订单类型走不同服务,应该拆到业务层或者用分发模式。
- 误区 2:网关自己做 HTTPS 证书管理。网关本身不存私钥,SSL 终结在 Nginx/ALB 层,网关只处理 HTTP 请求。
- 误区 3:所有鉴权都放网关。JWT 验签放网关是合理的,但细粒度权限(这个订单是不是这个用户的)应该在服务里做,那里有完整的业务上下文。
- 误区 4:滥用 GlobalFilter。全局过滤器越多,请求链路越长;能用路由级别的 GatewayFilter 就尽量用,只影响相关路由。
- 误区 5:忽略网关自身的健康检查。网关实例挂了,Nginx 还往它转发请求,所有经过它的服务都不可用。必须配健康检查,挂掉自动摘除。
小结:网关是微服务架构的"入口面",统一处理路由、鉴权、限流、灰度这些横切关注点,让后端服务只关心业务逻辑。下一讲 32. 可观测性三支柱 会讲服务上线后怎么监控——网关只能拦截请求,但服务内部出问题只能靠日志、指标和链路追踪。
参考
参考:Spring Cloud Gateway 官方文档(Route Predicate 与 Filter 工厂完整列表)、Spring 官方博客 Reactive 与 Servlet 对比