Inspirer

HTTP 中的幂等操作

在做 web 端开发,常常和各种请求打交道。HTTP 协议中,常见的请求不外乎是 GETPOSTPUTDELETEHEAD。其中,GET、PUT、DELTE、HEAD 都是幂等操作,而 POST 则不是。那么什么是幂等操作?其存在的意义和网站的设计与架构有什么关系呢?

幂等与非幂等

幂等通俗来说是指不管进重复多少次操作,其结果都一样。

GET 属于安全且幂等的操作,因为其并不改变资源,本身就是安全的,同样的,对任意资源发起 N 次同样的 GET 请求,其结果也是一致的,因而也是幂等操作。

PUT 和 DELETE 也属于幂等操作,也是安全的。

先说说幂等。PUT (广义来说)是对一个资源进行修改,比如修改一篇博客文章,将其标题由 A 改为 B,我们无论对该资源发起多少次同样的 PUT 请求,对该文章标题的影响是一样的,结果都是其标题变为 B。DELETE 也是一样。只不过当这类请求一旦成功,后续的同样请求、操作不会生效而已,但不会对结果产生影响。

再说说所谓的“安全”。因为操作的幂等,当我们无法确认一个操作是否成功(如发出一个 PUT 请求去修改一个资源,但却没有收到服务器响应),我们依旧可以放心的再发出多个同样的请求,而不用担心任何问题。所以我们说这个操作是“安全的”。

因此,我们说 POST 是不幂等、不安全的操作。

在创建一个资源的时候,我们用的最多的就是 POST,但是,当我们无法确认一个 POST 请求是否发送成功,我们并不能随意再次发出同样请求,因为这可能不经意创建出多个东西出来(每次请求都会产生新的东西或者说产生不同结果)。

这意味着我们在处理非幂等操作 POST 需要非常慎重,尤其是涉及诸如电子商务中的订单系统时,我们需要通过一系列方法规避重复进行有害的非幂等操作。

规避威胁

了解了不安全的非幂等操作带来的问题,我们也有必要强调对于 HTTP 请求的规范。

HTTP 语义其实已经告诉了我们 GET、 POST、 PUT、 DELETE 代表的实际意义,但大多数项目依旧只是用了 GET 和 POST,虽然我们知道语义化只是口头上的声明,至于实际请求的影响是由 Web 服务端程序决定的,但这样做这意味着放弃了通过区分请求方法来决定操作类型,这会在逻辑代码中造成过于繁杂的验证机制,以确保非幂等操作没有产生副作用,这对于逐渐复杂的项目的维护产生了严重的困扰。

当我们确保每一个请求类型符合其语义,我们仅仅需要区分操作的幂等性,即可省去不必要的验证机制。比如只要是幂等操作就可以放心的发出请求,对于非幂等操作单独做验证即可。

同样的,在项目中,完全去掉非幂等操作根本不可能,但我们需要尽量去掉不必要的非幂等操作,来减少不必要的麻烦。

如何解决非幂等操作的副作用

既然要解决非幂等操作的副作用,那就得确认其副作用是什么。

非幂等操作主要的问题在于重复的表单提交会产生不可预料的结果,也许重复发出评论、文章还有余地,对于重要的交易系统的订单、交易记录等等产生这类问题,必然是致命的。因此我们对于同一个来源下的非幂等操作请求,需要进行必要的限制。

例如作为某个已登录用户,和服务端之间有一个会话保存其状态,假如他发起一个 POST 请求,我们会对这个请求中的关键的差异数据进行记录(比如订单中的商品、购买数量等),当其再次发起一次 POST 请求,该请求中的数据和之前记录的关键数据一致时,则提示其可能存在重复提交的可能。

差异数据在这里指的是,正常提交时,在较短时间内多次提交上来的数据绝对不会完全一致,例如作为一个正常客户,你不可能在 5 秒之内反复创建同一个商品、选择同样数量、发出完全一致的订单需求等等。

但是为了保证这种识别不会出现意外,我们必须要记录足够的细节。当然,限制一段时间内的请求也是种比较好的办法。对于同一个客户,由于其正常操作流程最快也会需要小段时间,可以此作为限制手段。

当然,对于不同业务需求其解决办法不唯一,上述只是抛砖引玉之说,对于文章评论这类,非幂等操作的副作用其实并不明显的时候,也许仅仅一个发布需要验证码就够了 :blush: