我如何告知用户浏览器它发出的 POST 请求没有副作用?

How can I communicate to the user's browser that a POST request it made is side-effect-free?

我必须向我的网站添加一个页面,该页面将通过 POST 请求访问。该请求没有副作用,因此用户可以安全地使用页面上浏览器的 "Refresh" 按钮。它必须是 POST 而不是 GET 的原因是表征请求所需的数据量很大(它包括任意多个 GUID 的集合,这些 GUID 标识在流程的后期阶段要操作的资源).

当浏览器用户刷新由 POST 请求生成的页面时,浏览器通常会警告他们将重新提交表单并可能导致重复操作。在这种情况下这不是问题,因为正如我所说,请求此页面的操作没有副作用。因此,我想告知用户的浏览器,如果他们使用 "Refresh" 函数,则不应向用户显示此类警告。我该怎么做?

您可以通过实施 Post/Redirect/Get 模式来解决这个问题。

出于安全原因,当您尝试重新发送 POST 请求时,通常会收到浏览器警告。考虑一个表单,您可以在其中输入个人数据来注册帐户或订购产品。如果您要重复发送您的数据,则可能会发生您注册两次或购买同样的东西两次(当然,这只是一个理论上的例子)。因此,用户在尝试多次发送相同的 POST 请求时应该会收到警告。此行为是有意设计的,无法禁用,但可以通过使用上述 PRG 模式来避免。


图片来自 Wikipedia (published under LGPL).

简单来说,此模式可用于避免可能导致意外结果的表单数据的双重提交。您必须将服务器配置为使用 status code 303 ("see other") 重定向受影响的传入 POST 请求。然后用户将被重定向(使用 GET 请求)到确认页面,显示请求已成功,现在将被处理。如果用户现在重新加载页面,他/她将被重定向到同一页面,而无需重新提交 POST 请求。

但是,此策略可能并不总是有效。如果服务器还没有收到第一次提交(例如由于流量),如果用户现在重新提交第二次 POST 请求仍然可以发送。

如果您提供有关技术堆栈的更多信息,我可以通过添加特定代码示例来扩展我的答案。

您不能阻止浏览器警告用户重新提交 POST 请求。

参考资料
Mozzila 论坛(Firefox 的前身)discussed the feature 从 2002 年开始广泛使用。也出现了对其他浏览器的一些讨论。很明显,已做出强制执行该功能的决定,尽管提出了变通办法,但没有采用。

Google Chrome (2008) 和其他后续浏览器也包含该功能。

原因与 GET and POST in rfc2616: Hypertext Transfer Protocol -- HTTP/1.1 (1999) 之间的差异有关。

GET

retrieve whatever information is identified by the Request-URI

POST

request that the origin server accept the entity enclosed in the request as a new subordinate of the resource

这表明虽然 GET 请求仅检索数据,但 POST 请求会以某种方式修改数据。根据对 Mozilla forum 的讨论,决定是禁用警告给用户带来的风险比保留警告带来的不便更多。

解决方案
相反,一种解决方案是使用会话将数据存储在 POST 请求中,并使用 GET 请求将用户重定向到 URL,该 URL 在会话数据中查找原始请求参数。

假设服务器端应用程序具有会话支持并且已启用。

  1. 用户提交 POST 请求,其中包含生成特定结果的数据 POST /results
  2. 服务器使用已知密钥将该数据存储在会话中
  3. 服务器以 302 重定向响应所选 URL(可能是同一个)
  4. 客户端将使用 GET 请求请求新页面GET /results
  5. 服务器识别传入的 GET 请求正在询问先前 POST 请求的结果,并使用已知密钥从会话中检索数据。

如果用户刷新页面,则重复第 4 步和第 5 步。

为了使解决方案更加稳健,POST 数据可以分配给作为路径或查询的一部分在 302 重定向 GET /results?set=1 中传递的唯一键。这将使多个不同的页面能够被查看和刷新,例如在不同的浏览器选项卡中。必须考虑确保唯一密钥有效并且不允许访问其他会话数据。

一些系统、Kibana、Grafana、pastebin.com 和许多其他系统更进一步。 POST 请求值存储在持久数据存储中,并向用户提供唯一的短 URL。短 URL 可用于 GET 请求并与其他用户共享以查看与最初 POST 请求相同的结果。

您无法阻止所有浏览器显示“您确定要重新提交此表单吗?”当用户刷新作为 POST 请求结果的页面时弹出。因此,如果您想在用户在该页面上按 F5 时阻止此弹出窗口,则必须将此 POST 请求转换为 GET 请求。

对于搜索表单,您承认这是为了将 POST 转换为 GET 有其自身的问题。

首先,您确定需要 POST 才能开始吗?数据真的太大而无法放入查询字符串吗?取一个 reasonable limit of 1024 characters,大约有 30 个 GUID(给或取一些 space 用于重复 &q=),为什么需要搜索参数作为 GUID 开始?如果您可以映射它们或以某种方式查找它们,您也许可以将每个参数的大小限制为少数几个字符,而不是非虚线 GUID 的 32 个字符,并且每个键 5 个字符,您可以突然在查询中容纳 200 个参数字符串.

还不够吗?那你确实需要一个POST。

评论中提到的一种方法是使用 AJAX,因此您的搜索表单实际上并未提交,而是通过 JavaScript HTTP POST 请求并用结果更新页面。这样做的好处是刷新页面不会提示,因为就浏览器而言只有 GET,但有一个缺点:搜索结果没有得到唯一的 URL,所以你不能缓存、添加书签或分享

如果您不关心缓存或 URL 书签,那么 AJAX 绝对是这里最简单的选项,您无需进一步阅读。

对于所有非AJAX 方法,您需要在某处保留 查询参数,启用Post/Redirect/Get 模式。这种模式以 GET 请求的结果页面结束,用户可以在没有弹出窗口的情况下刷新该页面。其他答案相当handwavy的是如何正确地做到这一点。

选项是:

服务器端会话

当POST访问服务器时,你可以让服务器在session中持久化查询参数(所有主要的服务器端框架都允许你使用session),然后将用户重定向到一个通用的/search-results 页面,它在服务器端从会话中读取数据,并向用户显示查询数据库并结合会话中的查询参数构建的结果。

缺点:

  • 会话通常会超时,而且他们这样做是有充分理由的。如果您的用户在 20 分钟后点击 F5,他们的会话数据就会消失,他们的查询参数也会消失。
  • 会话在浏览器选项卡之间共享。如果您的用户正在选项卡 1 上搜索事物 A,并在选项卡 2 上搜索事物 B,则最新提交的选项卡的参数将在刷新时覆盖较早的选项卡。
  • 会话基于浏览器。通常没有简单的方法来共享会话(除了将会话 ID 放在 URL 中,但请参阅第一个项目符号),因此您不能添加书签或共享您的搜索结果。

本地存储/cookies

你可能会想“但是 cookie 可以包含比查询字符串更多的数据”,但事实并非如此。除了也有限制外,它们还在选项卡之间共享,不能(轻松地)在用户之间共享,也不能添加书签。

本地存储也不是一个选项,因为虽然它可以包含更多数据 - 但它不会发送到服务器。这是 本地 存储。

服务器端持久存储

如果您的搜索查询确实那么 复杂以至于您需要多个 KB 的查询参数,那么您可能可能受益于坚持在数据库中查询参数。

因此,对于每个搜索请求,您都会创建一个新的 search_query 数据库记录,其中包含用于执行查询的适当参数,并且如果搜索结果不是私有的,您甚至可以编写一些查找给定参数组合之前是否使用过并首先执行查找的代码。

因此您得到一个唯一的 search_id,它指向一组您可以用来执行查询的参数。现在您可以重定向您的用户,以便他们向该页面执行 GET 请求:

/search-results?search_id=Xxx

然后您将在此处呈现给定查询的结果。好处:

  • 您可以缓存、添加书签和共享 URL /search-results?search_id=Xxx
  • 您可以刷新显示搜索结果的页面,而不会出现烦人的弹出窗口
  • 每个浏览器选项卡显示自己的搜索结果

当然这种做法也有缺点:

  • 除非您为 search_id 使用不可猜测的密钥,否则用户可以枚举其他用户之前的搜索
  • 每次搜索都会花费永久服务器端存储空间,除非您决定根据某些条件驱逐较早的搜索