API 设计与网关
接口设计得好不好,半年后维护成本差十倍。这篇讲对外接口的契约和入口层(网关)的职责。
一、REST 设计规范
资源用名词,动作用 HTTP 方法
✅ GET /users/123
✅ POST /users
✅ PATCH /users/123
✅ DELETE /users/123
❌ GET /getUser?id=123
❌ POST /user/delete/123✅ GET /users/123
✅ POST /users
✅ PATCH /users/123
✅ DELETE /users/123
❌ GET /getUser?id=123
❌ POST /user/delete/123状态码要真的用起来
| 码 | 什么时候用 |
|---|---|
| 200 | 成功(含 PATCH 更新成功) |
| 201 | 创建成功(配 Location 头) |
| 204 | 成功但无返回体(DELETE) |
| 400 | 参数错误(客户端的问题) |
| 401 | 未认证(没登录/token 无效) |
| 403 | 已认证但无权(越权) |
| 404 | 资源不存在 |
| 409 | 冲突(幂等键重复、乐观锁版本冲突) |
| 429 | 触发限流(配 Retry-After) |
| 500 | 服务端错误 |
别全用 200 + {code: xxx} —— 那样监控、网关、SDK 全都得解析 body 才知道成败。
统一错误体
json
{
"code": "ORDER_NOT_FOUND",
"message": "订单不存在,请确认订单号",
"traceId": "ab12cd34",
"details": [{ "field": "orderNo", "reason": "not_found" }]
}{
"code": "ORDER_NOT_FOUND",
"message": "订单不存在,请确认订单号",
"traceId": "ab12cd34",
"details": [{ "field": "orderNo", "reason": "not_found" }]
}三个要点:机器可读的 code、人能看懂且可直接展示的 message、traceId 便于排查。
分页与过滤
GET /orders?page=2&size=20&status=PAID&sort=-createdAt
→ { "items": [...], "total": 137, "page": 2, "size": 20 }GET /orders?page=2&size=20&status=PAID&sort=-createdAt
→ { "items": [...], "total": 137, "page": 2, "size": 20 }大数据量用游标分页(?cursor=xxx)而不是深分页(原理同 Elasticsearch 的深分页)。
二、版本化
| 方式 | 例子 | 评价 |
|---|---|---|
| URL 路径 | /v1/orders | 最直观,推荐 |
| 请求头 | Accept: application/vnd.api.v2+json | 干净,但不好调试 |
| 查询参数 | ?version=2 | 不推荐,容易被缓存污染 |
兼容性原则:加字段是兼容的,删字段/改类型/改语义是破坏性的 —— 破坏性变更才升版本。
三、幂等设计(分布式必问)
为什么要幂等:客户端重试、网关超时重发、用户连点,都会造成重复请求。
| 方法 | 天然幂等吗 |
|---|---|
| GET / PUT / DELETE | 是(按语义) |
| POST | 否,必须自己实现 |
幂等键方案
1. 客户端生成 requestId(UUID),放在 header:X-Request-Id
2. 服务端:SETNX idempotent:{requestId} → 成功则处理,失败则返回上次结果
3. 处理完把结果缓存(TTL 24h),重复请求直接返回同一结果1. 客户端生成 requestId(UUID),放在 header:X-Request-Id
2. 服务端:SETNX idempotent:{requestId} → 成功则处理,失败则返回上次结果
3. 处理完把结果缓存(TTL 24h),重复请求直接返回同一结果java
public Order create(String requestId, CreateReq req) {
String key = "idem:" + requestId;
if (!redis.setIfAbsent(key, "1", Duration.ofHours(24))) {
return loadPreviousResult(requestId); // 重复请求:返回原结果,不是报错
}
try {
return doCreate(req);
} catch (Exception e) {
redis.delete(key); // 失败要释放,允许重试
throw e;
}
}public Order create(String requestId, CreateReq req) {
String key = "idem:" + requestId;
if (!redis.setIfAbsent(key, "1", Duration.ofHours(24))) {
return loadPreviousResult(requestId); // 重复请求:返回原结果,不是报错
}
try {
return doCreate(req);
} catch (Exception e) {
redis.delete(key); // 失败要释放,允许重试
throw e;
}
}三个易错点:
- 重复请求要返回成功的结果,而不是报「重复提交」;
- 失败必须释放幂等键,否则用户永远重试不了;
- 数据库唯一索引是最后一道防线(Redis 可能丢)。
四、鉴权方式对比
| 方式 | 适用 | 说明 |
|---|---|---|
| Session + Cookie | 传统 Web | 服务端存储,横向扩展要共享 session |
| JWT | 前后端分离、多端 | 无状态,但无法主动失效(要靠黑名单/短 TTL) |
| OAuth2 | 第三方授权 | 授权码模式最常用 |
| AK/SK 签名 | 开放 API / 服务间 | 见下节 |
JWT 的三个坑:不能放敏感信息(只 base64,不是加密)、无法主动登出(用短 access + refresh token)、payload 太大会拖慢每个请求。
五、开放 API 的签名与防重放
待签名串 = method + "\n" + path + "\n" + sortedParams + "\n" + timestamp + "\n" + nonce
signature = HMAC-SHA256(secret, 待签名串)待签名串 = method + "\n" + path + "\n" + sortedParams + "\n" + timestamp + "\n" + nonce
signature = HMAC-SHA256(secret, 待签名串)服务端校验四件事:
- timestamp 与当前时间差 < 5 分钟(防重放);
- nonce 未用过(Redis SETNX,TTL 10 分钟);
- 签名 一致;
- AK 有权限访问该接口。
六、网关的五项职责
客户端 → 网关(路由 / 鉴权 / 限流 / 灰度 / 日志)→ 下游服务客户端 → 网关(路由 / 鉴权 / 限流 / 灰度 / 日志)→ 下游服务| 职责 | 具体做什么 | 不做会怎样 |
|---|---|---|
| 路由 | 按 path/header 转发,做协议转换 | 客户端要记住 N 个服务地址 |
| 鉴权 | 统一验 token、验签,透传用户身份 | 每个服务都要写一遍鉴权 |
| 限流熔断 | 按用户/租户/接口限流,失败熔断 | 一个服务挂掉拖垮全站 |
| 灰度 | 按 header/比例把流量打到新版本 | 只能靠发版扩容做灰度 |
| 日志与追踪 | 统一记录 traceId、耗时、状态码 | 出问题不知道该找谁 |
网关不该做的事:写业务逻辑、做数据聚合(那是 BFF 的职责)、做重计算。
实现参考:Spring Cloud Gateway 的路由与过滤器。
七、灰度发布的四种做法
| 方式 | 粒度 | 实现 |
|---|---|---|
| 按比例 | 5% → 20% → 100% | 网关权重路由 |
| 按用户 | 白名单用户先体验 | 网关读用户标签 |
| 按请求头 | 内部测试带 X-Gray: 1 | 网关匹配 header |
| 按机房/地域 | 单机房先上 | DNS / 网关按 region |
灰度必须配套两件事:可比对的指标(新老版本的错误率、P99)和一键回滚。
八、接口设计的十条检查
- 资源用名词、层级不超过 3 层
- 状态码真实反映结果,别全 200
- 错误体有 code / message / traceId
- 列表接口必须有分页和上限(默认 20,最大 100)
- POST 有幂等键
- 时间统一 ISO8601 + 带时区,金额统一用「分」的整数
- 枚举值有文档,且不随意改语义
- 破坏性变更升版本号
- 敏感字段不入参/不出参(脱敏)
- 每个接口有超时和重试策略
九、速查
| 问题 | 答案 |
|---|---|
| REST 路径怎么写 | 名词 + HTTP 方法,别在 URL 里放动词 |
| 状态码要不要用 | 要用,401/403/409/429 都别退化成 200 |
| 幂等怎么实现 | 客户端 requestId + Redis SETNX + 数据库唯一索引兜底 |
| 重复请求返回什么 | 返回上次成功的结果,不是报错 |
| JWT 最大的问题 | 无法主动失效,只能短 TTL + 刷新令牌 |
| 网关做什么 | 路由、鉴权、限流熔断、灰度、日志追踪 |
| 网关不做什么 | 业务逻辑、数据聚合、重计算 |
| 开放 API 怎么防重放 | timestamp + nonce + 签名 |
Cao Note