payin商户操作手册

指南

TLS 0-RTT 早期数据与支付 POST:用 425 回应,而不是靠猜

为什么不安全的支付请求不应放进 TLS 1.3 早期数据,Early-Data 头与 425 Too Early 如何转移重试决策,以及 425 保护不到什么。

发布与来源核查:

发布与来源核查:2026-09-24。适用范围:位于“接收支付的 HTTP API”之前的 TLS 1.3 早期数据(0-RTT),协议规则取自 RFC 8470《Using Early Data in HTTP》(IETF Standards Track,2018 年 9 月)、RFC 8446《The Transport Layer Security (TLS) Version 1.3 Protocol》(2018 年 8 月)与 RFC 9110《HTTP Semantics》(2022 年 6 月)。本文是规范研究,不代表任何服务商的 TLS 配置,也不能替代对你自己的客户端、代理与服务端链路的测试。

支付 API 为什么要在意一个传输层特性

早期数据允许回头客在连接的第一趟飞行中就发出请求,握手尚未完成,首个调用因此少一个往返。规范同时直说代价:RFC 8446 指出“0-RTT 数据的安全属性弱于其他 TLS 数据”,包括“不同连接之间不存在不重放的保证”。[2]

RFC 8470 把这一点变成 HTTP 问题。客户端在被拒绝后乐观重发,可能被诱导发送两次:“自动重试带来重放攻击的可能……如果客户端重试曾在早期数据中发送的请求,该请求会被处理两次。”[1] 对一笔创建扣款的请求来说,“被处理两次”正是本文要讨论的失败模式。

只有安全方法可以进入早期数据

客户端规则只有一句,且没有例外:“在没有其他信息时,客户端 MAY 在早期数据中发送使用安全 HTTP 方法([RFC7231] 第 4.2.1 节)的请求,并且 MUST NOT 在早期数据中发送不安全方法(或安全性未知的方法)。”[1] RFC 8470 引用 RFC 7231 作为定义来源;RFC 7231 此后已被 RFC 9110 取代,其第 9.2.1 节给出同一组方法:“本规范定义的请求方法中,GET、HEAD、OPTIONS 和 TRACE 被定义为安全方法”,安全的含义是语义“基本上是只读的”,客户端“不请求、也不期望在源服务器上产生任何状态变化”。[3]

支付 API 的 POST、PUT 与 DELETE 不在其中,因此按规范,除非你有更好的理由且服务端同意,否则它们不应进入早期数据。由此得出的推论:查询支付状态的 GET 与创建支付的 POST 是两类情况,规范有意区别对待。RFC 8470 把选择权交给客户端:“从本质上说,客户端控制某个请求是否放入早期数据,也就控制了重放风险。”[1]

TLS 承诺了什么,又没有承诺什么

RFC 8470 并不认为早期数据毫无保护。它指出“TLS [TLS13] 规定了重放检测策略,用于降低攻击者成功重放早期数据的能力”,同时提醒“这些反重放技术只能降低、不能完全消除数据被重放的概率,并保证重放次数存在固定上限”。[1] RFC 8446 补上了边界条件:0-RTT 数据“在单个连接内不能被复制”,但跨连接时之所以“攻击者无法把 0-RTT 数据伪装成 1-RTT 数据”,靠的是密钥不同,而不是复制不可能发生。[2]

所以你得到的是 TLS 内部有上限的重放容忍度,而不是业务操作的唯一性。防重复扣款不是 TLS 的功能。

服务端有三种被规范列出的回答

RFC 8470 列出三种:“服务端可以在 TLS 层拒绝早期数据”——这会丢弃早期数据携带的所有请求;“服务端可以选择推迟处理早期数据,直到 TLS 握手完成”,这“让服务端获得早期数据未被重放的一定保证”;或者“服务端可以回应 425 (Too Early) 状态码,促使客户端重试个别请求且不使用早期数据”。[1] 文档随后说明“这些方法同样有效,服务端可以选择最适合自己的方式”,并且重放容忍度是按请求判定的。[1]

这就是需要在配置中一次性决定的事,对象是支付相关端点:推迟、拒绝,还是逐个拒绝。不能把它留给负载均衡器碰巧的行为。

请求头与状态码负责传递这个决定

Early-Data 请求头字段“表示该请求曾在早期数据中传递,且客户端理解 425 (Too Early) 状态码”,唯一合法值为“1”。[1] 中间节点转发可能被重放的请求时必须加上它,并且“如果请求中已存在该字段,中间节点 MUST NOT 移除”。[1] 对服务端的后果是严格的:“服务端无法仅靠等待握手完成,就让携带 Early-Data 头字段的请求变得可以安全处理……携带 Early-Data 头字段且无法安全处理的请求,必须以 425 (Too Early) 状态码拒绝。”[1]

状态码本身:“425 (Too Early) 状态码表示服务端不愿冒处理可能被重放的请求的风险。”[1] 它不能随意发出——“除非请求是在早期数据中收到,或 Early-Data 头字段为 1,否则服务端不应发出 425 状态码”——并且“425 状态码默认不可缓存”。[1]

客户端欠服务端什么

客户端有两项义务:使用早期数据的客户端“在收到 425 (Too Early) 状态码时 MUST 重试请求”;并且“用户代理 SHOULD 自动重试,但任何重试 MUST NOT 再放进早期数据。”[1] 换句话说,425 是把请求移出风险窗口而不是丢弃它,重试本身必须走已完成的连接。

现实里能依赖多少,需要打折扣。MDN 记录 425 属于“Limited availability”,因为“该特性不属于 Baseline:它在部分广泛使用的浏览器中不起作用”,并把早期数据描述为“允许客户端在连接的第一个往返中就向服务端发送数据,而不必等待 TLS 握手完成”。[4] 你无法控制的浏览器不能假定实现了这套重试约定,因此依赖 425 行为的跳转回跳 POST,必须在你实际支持的浏览器中测试。

425 保护不到什么

以下是基于上述文本的编辑推理,不是引用的要求。425 覆盖的是“在握手尚未完成的连接上、以早期数据到达”的请求;稍后到达、在已建立连接上重复到达、或在另一条连接上重复到达的请求,完全不在这个窗口内。状态码只说明服务端拒绝处理,它不指出先前哪一次尝试真正执行过,RFC 8470 也没有定义任何应用层去重机制。凡是“最多扣款一次”的要求,都要在保存转账记录的那一层,由能看见两次尝试的机制来保证。

操作上的理解:把早期数据当作只读流量的延迟优化;除非测量过必要性,不要让创建支付的请求进入其中;如果它们仍可能进入其中,就回应 425,或推迟到握手完成再处理,而不是乐观执行。

来源与限制

核查日期:2026-09-24。RFC 引文取自 RFC Editor 发布的文本;RFC 8446 与 RFC 9110 的证据文件已明确标注为所引章节的摘录。MDN 页面带有自身的“最后修改”日期。本文不对任何具体厂商、CDN、代理或网关的默认值作出判断;请核实自己的配置。

  1. [1] RFC 8470:在 HTTP 中使用早期数据 — 发布:2018 年 9 月(IETF Standards Track);核查:2026-09-24。
  2. [2] RFC 8446:TLS 1.3 协议 — 发布:2018 年 8 月(IETF Standards Track);核查:2026-09-24。
  3. [3] RFC 9110:HTTP 语义 — 发布:2022 年 6 月(IETF Standards Track,STD 97);核查:2026-09-24。
  4. [4] 425 Too Early - HTTP | MDN — 页面最后修改:2026 年 2 月 27 日;核查:2026-09-24。