授权与合规声明本文全部操作对象均为自建隔离靶场本机容器或隔离虚拟机涉及安全测试的环节必须以取得合法授权为前提。未经授权的渗透测试违反《中华人民共和国网络安全法》与《刑法》相关条款须承担相应法律责任。本文只讲环境配置、版本对照与靶场隔离不含任何攻击步骤、利用载荷与绕过手法请勿将文中环境指向任何非自有系统。一、先把三个词分清safe、幂等、可缓存1.1 抖一下之后两种完全相反的现场线上常见的一幕客户端把一个写请求发出去等到超时也没等到响应中间网络抖了一下。客户端里那段自动重发逻辑按设定把同一个请求又发了一遍。随后两种结果都可能出现。第一种列表里出现两条内容一样、时间只差几百毫秒的记录立刻有人说这是重复提交得加锁。第二种列表里只有一条于是有人得出另一个结论这个接口是幂等的POST 也能放心重发。两个结论都下得太快。你看到一条还是两条取决于服务端怎么处理这次请求不取决于方法名。方法名给的是「客户端可以怎么假设」不是「服务端一定怎么做」。这句话是本篇的地基。1.2 三个词问的不是同一件事safe、幂等、可缓存这三个词经常被当成一组近义词混着用。它们回答的问题完全不同词它在问什么答错之后会出什么事谁最在意safe这个方法的语义是不是本质上只读客户端会在用户没同意时发起带副作用的请求浏览器、爬虫、预取逻辑幂等同一请求发多次服务端被请求的效果是否和发一次相同断连之后不敢重发或者不该重发时重发了客户端重试、网关、SDK可缓存这个响应能否被中间层存下来复用缓存把不该复用的响应拿去复用了代理、CDN、浏览器缓存三者有包含关系但不是同一条线。safe 的方法一定幂等——这是 IDEM-02 的直接含义PUT、DELETE 和所有 safe 方法都在幂等集合里反过来幂等的方法不一定 safePUT 和 DELETE 就是例子。可缓存和另外两个基本正交它关心响应能不能被复用不关心请求重复执行会怎样。1.3 六个方法摆成一张矩阵先把结论摊开后面几章逐格解释方法safe幂等默认能否用缓存响应代替一句话GET是是能取回表示语义只读HEAD是是能与 GET 同但不带正文OPTIONS是是一般不能问这个资源支持什么TRACE是是一般不能回显收到的请求诊断用PUT否是不能用报文里的表示替换目标状态DELETE否是不能解除 URI 映射与当前功能的关联POST否否默认不能让目标资源按自己的语义处理PATCH否否请求可以做到不能按补丁部分修改第三列要说明一句POST 那一格有本文唯一的逐字依据就是 IDEM-07 里那句「a POST request cannot be satisfied by a cached POST response because POST is potentially unsafe」其余几格按 RFC 9110 缓存一节的通行表述给出定位本文不逐字引。PATCH 那格的括号原因在 3.3 节。本章可以带走的一句safe 说的是「能不能不问就发」幂等说的是「断了能不能再发」可缓存说的是「响应能不能被别人拿去用」三个问题三套答案。二、幂等说的是「效果」不是「响应」2.1 定义句要一个词一个词读RFC 9110 给幂等的定义只有一句但每个成分都在限定范围「A request method is considered “idempotent” if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request.」拆开看intended effect不是 actual effect规范约束的是请求打算达成的效果不是这次执行实际发生了什么on the server效果落在服务端multiple identical requests前提是请求相同同一个方法配不同报文本来就不叫同一个请求the same as the effect for a single such request比的是多次和一次不是两次和三次。2.2 日志、修订历史、别的副作用都不在射程内紧接着的一句把边界划得更死「the idempotent property only applies to what has been requested by the user; a server is free to log each request separately, retain a revision control history, or implement other non-idempotent side effects for each idempotent request.」这句话给了服务端很大的自由每来一次请求就记一条日志、留一个修订版本、做一个别的非幂等副作用都允许。前提只有一个——这些都不是用户请求的那件事。回到开头那个场面判断就清楚了。列表里多了一条业务记录是被请求的效果重复了不幂等日志多两行、审计多两条、修订历史多一个版本这些是服务端自己的记录与幂等无关。计数器加了两则要看它是不是用户请求的那件事用户点的是「点赞」加两次就不幂等计数器只是运维侧的访问统计它属于附带记录。这一层只能由接口设计者自己判断。2.3 三个层次分开看层次幂等管不管典型例子判断依据被请求的效果资源状态管记录被创建了几条、余额被扣了几次多次请求后的状态与一次是否相同响应本身不管第一次 201、第二次 200带不带 ETag定义句里没有响应这个词服务端附带记录不管访问日志、审计流水、修订历史IDEM-03 明确说服务端可以自由做IDEM-04 里还有半句正好印证第二行「even if the original request succeeded, though the response might differ」——原请求可能已经成功了尽管响应可能不同。本章可以带走的一句幂等约束的是「用户请求的那件事在服务端留下的状态」不是响应长什么样也不是服务端顺手记了几行日志。三、逐方法过一遍3.1 safe 的四个GET、HEAD、OPTIONS、TRACE「Request methods are considered “safe” if their defined semantics are essentially read-only」——注意 essentially 这个词它说的是「本质上是只读的」不是「绝对没有任何写入」。一个 GET 让服务端写了行访问日志它仍然 safe刷了个缓存条目通常也还是 safe。判据仍是第二章那条这些写入是不是用户请求的那件事。四个里 GET、HEAD 日常用OPTIONS 问资源支持什么TRACE 回显请求用于诊断。四个都 safe按 IDEM-02 那句话也都在幂等集合里。所以「GET 断了能不能重发」答案是能。幂等集合比 safe 集合大多出来的正是 PUT 和 DELETE。3.2 PUT 与 DELETE幂等但都不是「没有后果」「The PUT method requests that the state of the target resource be created or replaced with the state defined by the representation enclosed in the request message content.」两个动词created、replaced。状态由报文里那份表示决定不是在现有状态上叠一层。这就是 PUT 幂等的机制来源同一份报文发一次和发五次最终状态一样。所以判断一个 PUT 接口是不是真幂等不看它标了什么方法看它内部做了什么。服务端要是把 PUT 实现成「把报文追加到列表末尾」它已经不是在实现 PUT 的语义幂等性也不成立。DELETE 更容易被误解「requests that the origin server remove the association between the target resource and its current functionality. In effect, this method is similar to the “rm” command in UNIX: it expresses a deletion operation on the URI mapping of the origin server rather than an expectation that the previously associated information be deleted.」第一句说的是解除 URI 映射与当前功能之间的关联第二句补了关键的一笔它表达的是对 URI 映射的删除操作不是「之前关联的信息一定会被抹掉」。规范拿 rm 打比方——rm 一个文件删掉的是目录里那条名字到 inode 的关联。这就解释了 DELETE 为什么幂等第一次删完「目标资源已经不在那」成立第二次再删这个状态还是成立。响应可能从 204 变成 404但那是响应层面的差异按第二章的判据不算数。3.3 POST 与 PATCH一个不在集合里一个要分两句读POST 不在幂等集合里规范在讲缓存时顺带把原因说清了「a POST request cannot be satisfied by a cached POST response because POST is potentially unsafe」potentially unsafe「可能不安全」。这个措辞不是「POST 一定写数据」而是「规范不承诺 POST 的效果可以重复」。一个 POST 接口碰巧什么也没写它仍然不在幂等集合里因为规范给的是方法的默认假设。PATCH 有两句话必须一起读「PATCH is neither safe nor idempotent as defined by [RFC2616], Section 9.1.」「A PATCH request can be issued in such a way as to be idempotent」只读第一句会得出 PATCH 不能重试只读第二句会得出 PATCH 可以重试。两句都对说的是两件不同的事方法层面不保证具体请求可以做到。差别在补丁的内容不在方法名上。同样是 PATCH「把这个字段设成这个值」发几次结果一样可以做到幂等「把这个计数加一」发几次加几次不幂等。所以很多团队会在文档里给 PATCH 单独写一句「本接口幂等」或「不幂等」而不是让调用方猜——文档给的是这一个接口的实际情况那才是重试逻辑该看的。下面是 PUT 一次往返的报文形状。本文没有在本机发出任何真实请求这些是按规范形状写出来的示意不是抓包结果也不指向任何线上系统。⚠️代码待验证本机未发出请求仅示意报文形状PUT /notes/42 HTTP/1.1 Host: notes.example.internal Content-Type: text/plain; charsetutf-8 Content-Length: 11 hello world HTTP/1.1 204 No Content这个往返只能看出一件事请求体带的是目标状态响应体是空的。服务端到底替换了还是追加了报文里看不出来。本章可以带走的一句方法名给的是默认假设接口的真实幂等性由服务端的实现决定PATCH 尤其要两句一起读因为它把「方法不保证」和「请求可以做到」写在了同一段里。四、为什么协议要在意这件事4.1 断连之后的那一次自动重发幂等不是为了好看才写进规范的它解决的是一个很具体的工程问题「the request can be repeated automatically if a communication failure occurs before the client is able to read the server’s response」注意时间点before the client is able to read the server’s response。不是「请求失败之前」是「客户端读到响应之前」。这个限定描述的正是那种「不知道服务端做没做」的状态。规范接着举了 PUT 的例子PUT 发出去之后连接断了客户端可以换一条连接重发「even if the original request succeeded, though the response might differ」。重发可能撞上一个已经成功的操作但因为有幂等这个前提撞上去不会多出一次效果。连接怎么复用、持久连接什么时候断不在本篇范围。本篇只关心重发在语义上允不允许。4.2 非幂等的纪律SHOULD NOT 后面还有两个出口「A client SHOULD NOT automatically retry a request with a non-idempotent method unless it has some means to know that the request semantics are actually idempotent, regardless of the method, or some means to detect that the original request was never applied.」结构值得拆主句是默认不许自动重试出口一是「有办法知道这个请求的语义实际上是幂等的」注意后面那个 regardless of the method不管它标的是什么方法出口二是「有办法检测到原请求从未被应用」。出口一是本篇最容易被忽略的一句方法名是默认值不是天花板。一个 POST 接口如果被设计成幂等的客户端在有依据的情况下重发是合规的。但依据必须是能说清的东西——文档写了、服务端有去重、或者调用方能查到操作状态。出口二更实用能检测到原请求从未被应用就可以放心重发。比如请求根本没发出去或者服务端能明确回答「这个操作我这儿没有」。4.3 「结果未知」是最难的一格客户端的处境有三种不是两种成功拿到完整响应服务端说做完了明确失败拿到完整响应服务端说没做结果未知请求发出去了响应没回来或者回来了但不完整。第三种最容易被当成第二种。「超时就当失败走重试」这种写法对幂等的方法合理对非幂等的方法就是把「可能已经做了」当成「肯定没做」。⚠️代码待验证本机未发出请求以下为时刻示意不是抓包t0 客户端发出请求 t1 服务端收到开始处理 t2 服务端处理完成效果已经落在状态里 t3 响应离开服务端 t4 客户端读到完整响应 断点落在 t0~t1 之间请求没到效果没发生 → 结果确定没做 断点落在 t1~t4 之间效果可能已发生 → 结果未知 断点落在 t4 之后客户端已读到 → 结果确定做了从 t1 到 t4 这一整段客户端看到的都是「没拿到响应」但服务端的状态可能已经变了。重试决策的难点全在这一段里。本章可以带走的一句幂等不是给服务端省事的属性它是给客户端一个「结果未知时可以重发」的许可许可的边界由方法语义划定不能靠「发一次试试」去试探。五、重试决策的三层判据5.1 第一层方法在不在幂等集合里第一层最省事也最容易被跳过。拿到一个接口第一个问题不是「它会不会失败」而是「它标的是什么方法、默认在不在幂等集合里」。在集合内GET、HEAD、OPTIONS、TRACE、PUT、DELETE默认允许自动重发但要过第二层和第三层不在集合里POST以及没有特别设计过的 PATCH默认不允许除非能满足 4.2 节那两个出口之一。5.2 第二层失败落在哪一段判据按断点位置分四档断点位置客户端看到什么服务端可能处于什么状态幂等方法非幂等方法建连之前就失败请求一个字节都没发出去没收到可以重发可以重发属「原请求从未被应用」请求已发出响应头未收全只知道自己发过可能已执行可以重发默认不重发响应头已收全正文中断状态码已经读到已执行结果已知可以重发按状态码判断响应完整收到结果明确已执行按状态码按状态码这张表是本文的归纳条文依据只有两处IDEM-04 那句「读到响应之前」和 IDEM-05 那句「检测到原请求从未被应用」。其余格子是工程判断用时按自己系统改。5.3 第三层响应码说的是「失败」还是「不知道」4xx请求被理解了但被拒绝。重发通常还是一样的结果重试没有意义少数与时间有关的除外。408、429明确要求「过一会儿再来」是少数几个「重试被邀请」的状态码。5xx服务端自认没做成。但 5xx 不等于「没产生效果」——写操作可能已经落库然后在返回路径上出了问题。对非幂等的方法5xx 仍属结果不确定。连接中断、超时没有状态码结果未知。Retry-After就挂在这一层上。它的值可以是 HTTP-date也可以是秒数规范里delay-seconds 1*DIGIT是一个非负十进制整数。配在 503 上表示服务预计要不可用多久配在 3xx 上表示两次请求之间至少要等多久。⚠️代码待验证本机未发出请求仅示意响应形状HTTP/1.1 503 Service Unavailable Retry-After: 120 Content-Length: 0同一个头也可以写成日期形式形如Sun, 25 Oct 2026 06:30:00 GMT。两种都合法只认秒数的代码遇上日期形式会当成没有这个头。把三层串起来判读顺序是这样⚠️代码待验证本机未执行仅记录判读顺序printf%s\n第 1 步确认方法——它在不在幂等集合里printf%s\n第 2 步确认断点——请求发出去了没有响应收到了多少printf%s\n第 3 步确认状态码——是明确失败还是结果未知printf%s\n第 4 步三步都能给出答案才重发任何一步含糊就先不重发本章可以带走的一句重试不是「再发一次」是「在三层判据都能给出答案时再发一次」任何一层含糊就先不发。完整版对照资料六个方法在 safe、幂等、可缓存三列上的完整对照以及每个结论对应的 RFC 段落位置都整理在这一份里。放在资料包里扫码即可获取六、幂等键是怎么回事6.1 先把身份说清草案不是标准有一个名字经常出现在接口文档里Idempotency-Key本文按待验证项处理。截至 2026-10-25业界确实有这类草案约定——IETF 有一份关于这个请求头的草案文档draft-ietf-httpapi-idempotency-key-header但它不是已发布的标准。这个区别在落地时要守住它不是 RFC不能当规范条文引草案会改字段名、位置、服务端回应方式不同版本之间可能不一样。本文只写到「业内有这类草案约定」这一层不展开字段的具体语义。6.2 服务端自己兜的三种做法草案管不到的地方得靠服务端自己。常见的有三种都不依赖任何特定标准。第一种唯一约束。把「这是哪一次操作」落成一个标识给标识建唯一索引。第二次带着同一个标识来插入冲突服务端就知道这件事做过了把第一次的结果返回去。判断点在最底层不易被上层绕过。第二种状态机。先把操作落成一条「待处理」记录再推进到「已完成」。重复请求进来时先查没有就新建并执行有且未完成就等或者返回当前状态有且已完成就直接返回结果。好处是能区分「正在做」和「做完了」。第三种幂等重写。不改基础设施改接口语义把「加一」改成「设成某个值」和 PUT 是同一个思路。代价是有些场景改不了比如「每次调用都要新增一条流水」本质上就是要多次效果。做法能兜住什么兜不住什么唯一约束同一个标识的重复插入同一标识下内容不同的冲突状态机重复执行以及「正在执行中」的并发状态推进那一步本身失败的情况幂等重写语义层面的重复天生要求「每次都有新效果」的需求6.3 三个边界窗口、作用域、清理窗口。去重记录留多久留一天第二天带同一个标识来就是新操作留一年存储成本和冲突概率都要重新算。窗口多长等于定义了「多长时间内的重试算同一次」。作用域。标识按客户端算、按账号算、还是按接口算范围划小了换个入口进来的重试就兜不住范围划大了两个不相干的操作可能撞在一起被误判成重复。清理。失败的那一次要不要留记录失败就删掉重试进来会当成新操作重新执行——对幂等的方法没问题对非幂等的方法等于没兜。留着又得能区分「失败但可以重试」和「失败且不该重试」。⚠️代码待验证示意非真实实现收到请求带着一个操作标识 ├─ 标识不存在 → 新建记录待处理→ 执行 → 标记完成 → 返回结果 ├─ 标识存在未完成 → 不重复执行返回当前状态或让调用方稍后再来 ├─ 标识存在已完成 → 不重复执行返回第一次的结果 └─ 标识存在内容不同 → 冲突显式报错不能当成功本章可以带走的一句幂等键是「让服务端认出这是同一次操作」的记号不是「让请求自动变幂等」的魔法认不出来的那部分仍然要靠接口自己的语义来兜。七、给接口写一份「重试友好度」自查7.1 七个问题给一个接口做重试友好度评估问七个问题就够了顺序不要换这个接口标的是什么方法默认在不在幂等集合里如果不在服务端做了什么让它实际上幂等这对应 IDEM-05 的第一个出口。客户端有没有办法检测「原请求从未被应用」这对应第二个出口。失败落在哪一段能不能区分「没发出去」和「发出去了没回音」5xx 的时候服务端能不能保证「要么全做要么全不做」有没有Retry-After给的是秒数还是 HTTP-date去重窗口多长、作用域多大、失败记录怎么清理7.2 三个「以为幂等其实不幂等」的写法写法一方法名对实现不对。标了 PUT内部却把报文追加到列表里。这种接口在文档上看着完全合规实际每重发一次就多一条。写法二唯一约束建了冲突时返回 500。服务端确实挡住了重复客户端拿到的却是一次失败分不清「已经做过了」和「真的做失败了」。前者该当成功后者该排查。冲突时返回的应该是能表达「这件事已经完成」的结果。写法三去重键用请求体的哈希而请求体里带了时间戳。每次生成的内容都不一样哈希自然不一样去重永远命不中。这种问题在联调阶段很难发现因为正常流程下确实只有一次请求。三个写法的共同点是幂等性是在实现里成立的不是在校验里成立的。方法名和文档都只是声明能不能扛住重发要看服务端真的做了什么。7.3 落地顺序先判方法把接口按「默认可重发」「默认不可重发」分两堆。再补服务端兜底不可重发的那堆里值得补的补唯一约束或状态机不值得补的在文档里写清楚让调用方别猜。最后才打开客户端的自动重试。顺序反了等于拿真实流量做实验。把七个问题收成一张表自查问题合格的样子不合格的信号方法在幂等集合里吗方法与语义一致标了 PUT实现是追加不在集合里有兜底吗唯一约束或状态机冲突时能表达「已完成」冲突时返回笼统失败能检测「从未被应用」吗能查到操作是否存在只有「成功」和「失败」两种回答失败阶段能区分吗能区分没发出、发出没回音、收到不完整一律按超时处理5xx 时的原子性要么全做要么全不做部分完成且没有标记有没有 Retry-After有且形式明确秒数或日期让调用方自己猜间隔去重窗口与作用域有明确数值和范围说不清留多久、按什么算本章可以带走的一句重试友好度不是文档里的一句「本接口幂等」是方法、实现、状态码、去重窗口四样东西对得上对不上的地方就是将来重复数据长出来的地方。完整版对照资料这份自查表连同判断依据的原文段落做成了可以逐项打勾的版本。放在资料包里扫码即可获取附表 A本文引用事实与官方出处对照表序号事实出处核验日期本文位置1幂等定义多次相同请求对服务端的 intended effect 与一次相同RFC 9110 §9.2.2IDEM-012026-10-25第二章 2.12PUT、DELETE 与 safe 方法属于幂等集合RFC 9110 §9.2.2IDEM-022026-10-25第一章 1.23幂等只约束被请求的效果服务端可自由记日志、留修订历史RFC 9110 §9.2.2IDEM-032026-10-25第二章 2.24通信失败发生在读到响应之前时请求可被自动重发RFC 9110 §9.2.2IDEM-042026-10-25第四章 4.15重发可能撞上已成功的原请求「though the response might differ」RFC 9110 §9.2.2IDEM-042026-10-25第二章 2.36非幂等方法默认不得自动重试及其两个例外RFC 9110 §9.2.2IDEM-052026-10-25第四章 4.27safe 的定义GET、HEAD、OPTIONS、TRACE 为 safeRFC 9110 §9.2.1IDEM-062026-10-25第三章 3.18幂等集合比 safe 集合大PUT、DELETE 幂等但不 safeRFC 9110 §9.2.2IDEM-062026-10-25第一章 1.29POST 是 potentially unsafe不能被缓存的 POST 响应满足RFC 9110 §9.3.3IDEM-072026-10-25第三章 3.310PUT 的语义把目标资源状态创建或替换为报文表示定义的状态RFC 9110 §9.3.4IDEM-082026-10-25第三章 3.211DELETE 的语义解除 URI 映射与当前功能的关联类比 rmRFC 9110 §9.3.5IDEM-092026-10-25第三章 3.212PATCH 既不 safe 也不幂等RFC 5789 §2IDEM-102026-10-25第三章 3.313「A PATCH request can be issued in such a way as to be idempotent」RFC 5789 §2IDEM-102026-10-25第三章 3.314Retry-After的值可以是 HTTP-date 或秒数RFC 9110 §10.2.3IDEM-112026-10-25第五章 5.315delay-seconds 1*DIGIT为非负十进制整数RFC 9110 §10.2.3IDEM-112026-10-25第五章 5.316Retry-After配 503 表示服务预计不可用多久RFC 9110 §10.2.3IDEM-112026-10-25第五章 5.317Retry-After配 3xx 表示两次请求之间至少等多久RFC 9110 §10.2.3IDEM-112026-10-25第五章 5.318重试判据三层方法 / 失败阶段 / 响应码为本文归纳本文归纳依据 IDEM-122026-10-25第五章 5.1 至 5.319失败阶段与能否重发的对应表为工程归纳非规范原文本文归纳IDEM-04、IDEM-052026-10-25第五章 5.220待验证Idempotency-Key是 IETF 草案不是已发布标准draft-ietf-httpapi-idempotency-key-headerIDEM-132026-10-25第六章 6.121本文未在本机发出任何 HTTP 请求报文块均为示意本批纪律2026-10-25第三章 3.322不指向任何真实业务接口本批纪律列明不写的内容全文未涉及本批纪律IDEM-142026-10-25全文23连接复用与持久连接不在本篇范围本批纪律2026-10-25第四章 4.1附表 B术语速查表术语一句话解释safe方法的定义语义本质上只读GET、HEAD、OPTIONS、TRACE 属此类幂等同一请求发多次服务端被请求的效果与发一次相同不比响应intended effect请求打算达成的效果幂等定义里用的就是这个词结果未知请求已发出、响应未完整收到服务端可能已执行幂等键让服务端认出「这是同一次操作」的标记相关草案尚未成为标准幂等重写把「加一」这类非幂等操作改写成「设成某个值」Retry-After告诉调用方多久之后再来值可以是秒数或 HTTP-datepotentially unsafe规范对 POST 的定性意思是效果不承诺可重复去重窗口服务端保留操作记录的时长超出窗口的重试算新操作三层判据方法、失败阶段、响应码三者都能给出答案才允许重发写在最后这篇用到的资料写这篇文章时我把 RFC 9110 里讲方法语义的那几段和 RFC 5789 里 PATCH 那一节对着读了很多遍最费劲的是把「响应不一样」和「效果不一样」这两件事彻底分开顺手也整理了几份配套的东西靶场环境对照表DVWA、upload-labs 在 Windows / macOS / Linux 三平台的可行性与推荐路径Web 安全学习路线图从基础打牢到安全管理四个阶段各学什么常用靶场清单每个靶场练什么、适合哪个阶段资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「靶场」优先通过。拿到之后建议先看环境对照表那一份再回头把自己项目里那几个带自动重试的接口按第五章的三层判据过一遍。