Skip to content

安全与幂等操作

本章导读:"安全"与"幂等"是 RFC 9110 中定义最严格、也是被误读最多的两个概念。它们不是形容词,而是决定中间件能否自动重试、预取、缓存、负载均衡的开关。本章给出形式化定义、判定方法与工程实现(幂等键、乐观锁)。

1. RFC 9110 的原文定义

安全方法(§9.2.1)

"安全"是指客户端**无意(do not intend)**引起副作用的方法。安全方法应当被视为只读,并可由其它系统(如搜索引擎、代理预取)自动"发起"而无需担心。

幂等方法(§9.2.2)

无论重复执行 N 次(N>1)还是仅执行 1 次,对服务器状态的最终影响都是等价的幂等方法。

两个定义里藏着三个常被忽略的细节:

  1. 安全看"意图":副作用可能客观存在(GET 记录访问日志、增加计数器),但只要设计意图是只读,即算安全。反过来,"意图更新"的 GET 再"无害"也不安全。
  2. 幂等比较的是"状态影响",不是响应DELETE /a → 204DELETE /a → 404——两次响应不同,但资源终态一致(a 不存在),故 DELETE 幂等。updatedAt 每次刷新、审计日志多一条,也不破坏幂等(RFC 9110 允许服务器保留重复检测信息)。
  3. 幂等 ≠ 每次结果相同:并发下 PUT /a {v:1}PUT /a {v:2} 交错,"最后写赢"——单个请求序列各自幂等,不代表无并发问题,所以还需要条件请求(见 §5)。

2. 判定矩阵与记忆方法

方法安全?幂等?直觉解释
GET读,不改东西
HEAD只读的 GET 头版
OPTIONS探测元数据
TRACE回显
PUT设定为目标值:"设为 5"说几遍都是 5
DELETE"删掉它"说几遍结果都是没了
POST"再生成一个订单号"每遍都不一样
PATCH✘(默认)"价格 +10"不幂等;"价格=100"幂等——由补丁内容决定,RFC 5789 因此不承诺幂等

一句话记忆: 幂等是"设定值"语义(PUT/DELETE),非幂等是"求变化"语义(POST/部分 PATCH)。

3. 为什么这两个属性"值钱"

HTTP 生态围绕安全/幂等构建了一整套自动化行为,用对了白捡三大能力:

3.1 自动重试

网络超时(客户端不知道请求是否到达)时:

  • 安全/幂等方法:客户端、SDK、网关可放心重发——gRPC、云厂商 SDK 的默认重试策略就建立在此之上。
  • POST:重发可能重复下单——所以支付领域必须自建幂等机制(§4)。

3.2 预取与负载均衡

  • 浏览器/HTTP/2 服务器可对 GET 做 preload、推测性预取;CDN 可代答。
  • 幂等请求可被网关在实例故障时透明转移重试;非幂等请求只能"失败即未处理"。

3.3 缓存

只有安全方法(实质是 GET/HEAD)的响应会被共享缓存存储(RFC 9111)——这就是"查询别用 POST"的性能账

WARNING

反例:POST /search 包一切查询。后果:无法被任何缓存层加速;监控看不到"读流量"(全在 POST 里);网关不敢重试真正幂等的读请求。搜索接口应设计为 GET /articles?q=…,确有超长查询串需求再补一个 POST /searches(把"一次搜索"建模为资源)。

4. POST 的幂等化:Idempotency-Key

场景:客户端 POST /orders 后网络超时——到底创建成功没有?重试会不会双下单?

解法:客户端生成幂等键(业界通行做法,IETF 有专门草案 draft-ietf-httpapi-idempotency-key-header,Stripe/PayPal/Azure 均已产品化):

http
POST /orders HTTP/1.1
Idempotency-Key: 7c9e6679-7425-40de-988b-7bfdf48f1a2c   ← 一次"逻辑操作"唯一
Content-Type: application/json

{ "sku": "BOOK-1", "count": 2 }

服务端行为规范:

  1. 首次请求:执行业务,将 (key → 响应) 存入带 TTL 的缓存/表(如 24h),返回 201。
  2. 重试(相同 key、相同请求指纹):不再执行,直接重放首次响应(含原状态码与 body)。
  3. key 复用但 body 不同:返回 422 Unprocessable Content(或 409,团队统一约定)。
  4. 并发到达的同 key 请求:第二个等待首个完成,或返回 409 Conflict
  5. 处理中崩溃:缓存里没有完整响应 → 允许重试真正执行(配合业务层"防重唯一索引"兜底)。
时间线:客户端 ──POST(key=K)──► 服务器(成功但响应丢失)
        客户端 ──POST(key=K)──► 服务器:命中 K 的存档 → 原样返回 201 ✓(不重复扣款)

键由客户端生成、生命周期与一次业务意图绑定(用户点一次"提交"= 一个 key),这与"每次网络传输一个 key"有本质区别——前者才能让重试命中。

5. 并发的幂等不等于安全:乐观锁

幂等保证"同请求重复"无害;并发不同请求相互覆盖要靠条件请求(RFC 9110 §13.2 预条件头):

http
GET /articles/42
ETag: "v3"                          ← 服务器给资源表示打版本指纹

PATCH /articles/42 HTTP/1.1
If-Match: "v3"                      ← 我基于 v3 修改
{ "title": "新标题" }

HTTP/1.1 200 OK
ETag: "v4"
(若当前已是 v4412 Precondition Failed,客户端合并后重试)
  • 写接口强制或鼓励 If-Match:弱一致场景可省略(后写覆盖),协作编辑/配置类资源应强制。
  • 无 ETag 体系时,用资源内 version 字段承载同等语义,412/409 择一返回,文档写死。

6. 设计自检清单

  • [ ] 所有"读取"接口都是 GET,且无业务副作用(日志/指标除外)
  • [ ] 没有任何 GET 会修改数据(哪怕"只是刷新缓存")
  • [ ] PUT 携带完整表示;未提供的可空字段会被清空(文档声明)
  • [ ] PATCH 的赋值型补丁与运算型补丁分开设计;运算型补丁文档标注"不幂等"
  • [ ] 创建/支付/扣减等 POST 支持 Idempotency-Key(或请求体内 clientRequestId 字段,团队二选一)
  • [ ] 写接口支持 If-Match/版本号,冲突返回 412/409
  • [ ] 网关重试策略:GET/HEAD/PUT/DELETE 允许自动重试;POST 默认关闭(除非带幂等键)

7. 本章小结

  • 安全 = 意图只读(可被外界自动发起);幂等 = 重复执行终态等价(可被自动重试)。
  • 两者的价值在于解锁整个 HTTP 生态的自动化能力:缓存、预取、重试、故障转移。
  • POST 的幂等化靠客户端 Idempotency-Key + 服务端响应存档;并发覆盖防护靠 If-Match 乐观锁。

8. 下一步

概念篇完成。进入第 03 篇,把资源模型落成具体的 URI 规范 → URI 设计规范