设计 API 网关:认证、路由与限流
什么是 API 网关
API 网关是微服务架构中的统一入口,位于客户端与后端服务之间。它负责接收所有外部请求,执行公共的横切关注点——例如身份验证、请求路由、速率限制、日志记录和协议转换——然后将请求转发到相应的内部服务。将 API 网关视为系统的“前门”,没有它,每个客户端都需要独立处理认证、与多个服务通信及容错逻辑,导致代码重复和管理混乱。
对于初学者,可以从一个简单的类比开始:API 网关就像办公楼的接待处。所有访客首先到达接待处,接待员验证身份(认证),询问要拜访的部门(路由),并控制同一时间进入的人数(限流),然后指引访客去往正确的楼层。这样,各个部门不需要自己处理安保和导引工作。
网关核心功能设计
一个可投入生产的 API 网关至少需要具备以下能力,我们将聚焦于认证、路由和限流进行深入设计。
- 认证与授权:验证客户端身份,并检查其是否有权限访问请求的资源。
- 动态路由:根据请求的路径、头部或参数,将请求转发到正确的上游服务。
- 限流与熔断:保护后端服务免受过载,防止某个客户端消耗过多资源。
- 请求/响应转换:修改请求格式、添加或删除头部,或将后端响应聚合为客户端友好的格式。
- 可观测性:集中记录日志、监控指标和分布式追踪。
认证设计
API 网关中的认证通常基于令牌,以避免在每次请求中传输敏感凭证,并实现无状态验证。
常见认证流程
- 客户端向认证服务(或网关上的认证端点)发送凭据(例如用户名/密码)。
- 认证服务验证凭据,返回访问令牌(如 JWT)和可选的刷新令牌。
- 客户端在后续请求的
Authorization头部中携带访问令牌。 - API 网关拦截请求,验证令牌的有效性、签名和过期时间。
- 验证通过后,网关可以将令牌中包含的用户信息(如用户 ID、角色)注入请求头部,传递给上游服务,服务信任这些头部即可,无需再次解析令牌。
网关中的验证逻辑
不要在网关中进行复杂的权限判断(如“用户是否有权限编辑这篇文章”),这类授权逻辑更适合放在具体的服务中。网关应只负责令牌有效性验证,并提取主体身份。
伪代码示例(Node.js 风格):
async function authenticate(request) {
const authHeader = request.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return { valid: false, error: 'Missing token' };
}
const token = authHeader.split(' ')[1];
try {
const decoded = await verifyToken(token, publicKey);
// 将身份注入请求头部
request.headers['x-user-id'] = decoded.sub;
request.headers['x-user-roles'] = decoded.roles.join(',');
return { valid: true, identity: decoded };
} catch (err) {
return { valid: false, error: 'Invalid or expired token' };
}
}
安全建议
- 使用非对称签名(RS256/ES256),网关只需公钥即可验证令牌,私钥由认证服务独占。
- 令牌有效期应较短(例如 15 分钟),结合刷新令牌维持长期会话。
- 始终通过 HTTPS 传输,防止令牌被中间人截获。
路由设计
路由负责将请求映射到正确的后端服务实例。基本的路由匹配规则基于路径前缀,但实际网关需要支持更灵活的匹配策略。
路由配置模型
一种简单的路由表可以用 YAML 或 JSON 定义,并由网关动态加载:
routes:
- id: user-service
path: /api/users/**
target: http://user-service:8080
stripPrefix: /api/users # 转发时移除前缀
methods: [GET, POST, PUT]
- id: order-service
path: /api/orders/**
target: http://order-service:8081
stripPrefix: /api/orders
- id: legacy-service
host: legacy.example.com
path: /**
target: http://legacy-internal:9000
动态路由与服务发现
在容器化环境中,服务实例可能随时增减。路由应与服务发现系统(如 Consul、Eureka 或 Kubernetes Service)集成:
- 网关定期从服务注册中心拉取可用服务列表。
- 将路由目标解析为实际实例地址列表(例如
user-service → 10.0.1.5:8080, 10.0.1.6:8080)。 - 结合客户端负载均衡(轮询、最少连接)选择目标实例。
伪代码解析路径匹配:
function matchRoute(request) {
const path = request.path;
const method = request.method;
for (const route of routes) {
if (route.methods && !route.methods.includes(method)) continue;
const pattern = route.path.replace(/\*\*/g, '.+').replace(/\*/g, '[^/]+');
const regex = new RegExp(`^${pattern}$`);
if (regex.test(path)) {
// 提取路径参数(可选)
return route;
}
}
return null;
}
请求转发处理
转发时需注意:
- 设置合理的超时,避免上游服务无响应时耗尽网关资源。
- 重试策略:对幂等请求(GET、PUT)才能安全重试,并限制最大重试次数。
- 如果
stripPrefix启用,需要重写path。同时,保持原始Host头或替换为内部服务的真实 Host。
限流设计
限流防止恶意流量或意外突发流量压垮后端服务。常见算法包括固定窗口、滑动窗口、令牌桶和漏桶。对于 API 网关,滑动窗口日志或令牌桶算法效果较好,既能平滑流量又相对简单。
常见限流粒度
- 全局网关级别:限制进入网关的总请求数。
- 每个路由/服务:针对特定上游的配额。
- 每个客户端/用户:根据认证后的用户 ID 或客户端 IP 限制。
- 每个端点/HTTP 方法:例如限制登录接口的尝试次数。
滑动窗口计数器实现思路
使用 Redis 存储每个限流键的请求时间戳有序集合,高效实现滑动窗口。
伪代码(限流检查函数):
async function checkRateLimit(key, limit, windowInSeconds) {
const now = Date.now();
const windowStart = now - windowInSeconds * 1000;
// 删除窗口之外的旧记录
await redis.zremrangebyscore(key, 0, windowStart);
// 获取当前窗口内的请求计数
const count = await redis.zcard(key);
if (count >= limit) {
return { allowed: false, retryAfter: windowInSeconds };
}
// 记录本次请求,成员可以是唯一ID,score为当前时间戳
await redis.zadd(key, now, `${now}-${Math.random()}`);
// 设置键过期时间,避免内存泄漏
await redis.expire(key, windowInSeconds + 1);
return { allowed: true };
}
返回合适的响应
当请求被限流时,网关应返回 HTTP 429 Too Many Requests,并在响应头部中包含有用信息:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1617800000
高级策略:分层限流
设计多种限流器组合使用:
- 先进行基于 IP 的宽容限流(例如每秒 200 个请求),挡住基本滥用。
- 再根据用户 ID 进行精细限流(例如每分钟 60 个请求)。
- 对高价值接口(如支付)设置独立低阈值。
这样即使某个用户通过代理更换 IP,也无法绕过用户级配额。
统一请求处理管道
一个典型的网关请求处理管道如下,所有过滤器按顺序执行:
- TLS终结:网关处理 HTTPS,后端服务可用 HTTP。
- IP 黑/白名单检查:可选,直接在网络层拒绝。
- 认证过滤器:解析并验证令牌。
- 限流过滤器:检查是否超出配额。
- 路由匹配:确定上游服务和端点。
- 请求改写:添加身份头部、去除敏感数据、路径重写等。
- 负载均衡:选择具体的后端实例。
- 转发请求并接收响应。
- 响应后处理:添加安全头部(CORS、CSP)、压缩、记录访问日志。
初学者实现途径与工具
如果你打算自己实现而不是依赖现成产品,可以从这些起点开始:
- 使用现成框架:Spring Cloud Gateway (Java)、Kong (基于 OpenResty/Lua)、Traefik (Go)、Ocelot (C#)。这些工具已经内置了插件式的认证、路由和限流。
- 从头构建学习:使用 Node.js + Express 或 Python + FastAPI 搭建一个简单的网关,逐步集成
jsonwebtoken库、http-proxy和 Redis 限流器,深刻理解原理。 - 避免过度设计:初期只需支持静态配置文件中的路由、简单的 JWT 验证和内存固定窗口限流(单实例场景),之后再引入服务发现和分布式限流。
总结
设计 API 网关时,需要将认证、路由和限流作为三个独立但协作的模块来实现。认证保护资源访问,路由确保请求到达正确的位置,限流保护系统稳定性。始终记住保持网关的无状态性,将业务授权交给上游服务,并为核心功能设计清晰的配置接口和可观测性,这样你构建的网关才能从简单的“请求转发器”进化为架构中可靠的守门人。