【问题标题】:What is a good method for preventing a user from submitting a form twice?防止用户两次提交表单的好方法是什么?
【发布时间】:2010-01-21 19:32:32
【问题描述】:

我有一个购买页面,我不希望用户在进入“订单完成”页面后能够刷新页面并重新提交表单,因为它会通过数据库值在我们的系统中自动设置它们和通过贝宝向他们的卡收费(只希望这些发生一次)......我看到一些网站说“不要点击刷新,否则你会被收取两次费用!”但这很蹩脚,让它接受可能性,什么是只允许提交一次或阻止它们刷新等的好方法?

PS:我看到了一些类似的问题:PHP: Stop a Form from being accidentally reprocessed when Back is pressed 和 How do I stop the Back and Refresh buttons from resubmitting my form?,但没有找到令人满意的答案……如果有这样的机制,ASP.NET MVC 特定的答案也将是理想的。

编辑:一旦他们点击提交,就会将其发布到我的控制器,然后控制器会做一些魔术,然后返回一个带有订单完成消息的视图,但是如果我在浏览器上点击刷新,它会执行整个“你想重新发送”吗这个表格?这很糟糕......

【问题讨论】:

  • 如果您将其标记为与语言无关,您可能会得到更多答案,因为受影响的不仅仅是 ASP.NET MVC,还有与 Web 表单交互的所有语言和框架。 (当然,如果你把 ASP.NET MVC 标签留在那里,你可能会得到代码)。

标签: c# asp.net-mvc form-submit


【解决方案1】:

对此的标准解决方案是POST/REDIRECT/GET 模式。这种模式几乎可以使用任何 Web 开发平台来实现。你通常会:

  • POST 后验证提交
  • 如果失败,则重新呈现原始输入表单并显示验证错误
  • 如果成功,重定向到确认页面或重新显示输入的页面 - 这是 GET 部分
  • 由于最后一个操作是 GET,如果用户此时刷新,则不会重新提交表单。

【讨论】:

  • 在 ASP.Net MVC 中,只需执行“return RedirectToAction()”即可执行。
  • 除了来自表单请求处理操作的重定向之外,您如何避免该操作被其他任何东西击中?
  • @Maritim 我从来没有这样做过。我通常会返回标准视图页面或“谢谢”页面,但您可以传递页面期望的查询字符串参数,或设置会话变量以允许下一个请求进入。
  • @RedFilter 我想通了。您可以在 POST 处理操作中设置一个 TempData 变量,并在您进行确认操作时检查该变量是否存在 :) TempData 变量仅在一个操作到下一个操作之间存在,并且旨在在您使用 RedirectToAction
  • @Maritim 我打算这么建议,但不记得他们叫什么了 :)
【解决方案2】:

我 100% 同意 RedFilter 的通用答案,但想专门为 ASP.NET MVC 发布一些相关代码。

您可以使用Post/Redirect/Get (PRG) Pattern 来解决双重回发问题。

这是问题的图形说明:

当用户点击刷新时,浏览器会尝试重新提交它发出的最后一个请求。如果最后一个请求是一个帖子,浏览器会尝试这样做。

大多数浏览器知道这通常不是用户想要做的,所以会自动询问:

Chrome - 您正在查找的页面使用了您输入的信息。 返回该页面可能会导致您重复执行的任何操作。 您要继续吗?
Firefox - 要显示此页面,Firefox 必须发送信息以重复之前执行的任何操作(例如搜索或订单确认)。
Safari - 您确定要再次发送表单吗? 要重新打开此页面,Safari 必须重新发送表单。这可能会导致重复购买、cmets 或其他操作。
Internet Explorer - 要再次显示网页,网络浏览器需要 重新发送您之前提交的信息。 如果您正在购买,您应该单击取消以 避免重复交易。否则,单击重试以显示 再次访问网页。

但是 PRG 模式通过向客户端发送重定向消息来帮助完全避免这种情况,因此当页面最终出现时,浏览器执行的最后一个请求是对新资源的 GET 请求。

这是一个great article on PRG,它提供了 MVC 模式的实现。需要注意的是,您只想在服务器上执行非idempotent 操作时才使用重定向。换句话说,如果你有一个有效的模型并且实际上已经以某种方式持久化了数据,那么确保请求不会被意外提交两次是很重要的。但如果模型无效,则应返回当前页面和模型,以便用户进行必要的修改。

这是一个示例控制器:

[HttpGet]
public ActionResult Edit(int id) {
    var model = new EditModel();
    //...
    return View(model);
}

[HttpPost]
public ActionResult Edit(EditModel model) {
    if (ModelState.IsValid) {
        product = repository.SaveOrUpdate(model);
        return RedirectToAction("Details", new { id = product.Id });
    }
    return View(model);
}

[HttpGet]
public ActionResult Details(int id) {
    var model = new DetailModel();
    //...
    return View(model);
}

【讨论】:

    【解决方案3】:

    在提供订单确认页面时,您可以设置一个也存储在数据库/缓存中的令牌。在订单确认的第一个实例中,检查此令牌的存在并清除令牌。如果使用线程安全实现,您将无法提交两次订单。

    这只是众多可能的方法之一。

    【讨论】:

    • 作为一个额外的好处,这个解决方案还可以防止由于用户点击提交按钮两次而导致的重复表单提交。
    【解决方案4】:

    请注意,PRG 模式并不能完全防止多个表单提交,因为甚至在单个重定向发生之前就可以触发多个 post 请求 - 这可能导致您的表单提交不是 idempotent。

    请注意here提供的答案,它提供了解决此问题的方法,为方便起见,我在此引用:

    如果您在表单中使用了隐藏的防伪令牌(当您 应该),您可以在首次提交时缓存防伪令牌并 如果需要,从缓存中删除令牌,或使缓存条目过期 在设定的时间后。

    然后,您将能够根据缓存检查每个请求 特定表单是否已提交,如果已提交则拒绝。

    您不需要生成自己的 GUID,因为这已经完成 生成防伪令牌时。

    【讨论】:

      【解决方案5】:

      首次加载页面时,为每个访问者的表单提供一个唯一 ID。提交表单时记下 ID。使用该 ID 提交表单后,不允许任何进一步的请求使用它。如果他们点击刷新,将发送相同的 ID。

      【讨论】:

        【解决方案6】:

        只需从执行所有令人讨厌的事情的页面重定向到“感谢您的订单”页面。完成此操作后,用户可以根据需要多次点击刷新。

        【讨论】:

          【解决方案7】:

          如果你不喜欢将用户重定向到其他页面,那么使用我的方式你不需要Post/Redirect/Get (PRG) 模式 并且用户留在当前页面而不用担心重新提交表单的负面影响!

          我使用TempData 项目和Hidden field(表单的ViewModel 中的属性)在两侧(Server/Client)保持相同的Guid,这是我检测是否表单是否通过刷新重新提交。

          代码的最终面看起来非常简短:

          行动:

          [HttpPost]
          public virtual ActionResult Order(OrderViewModel vModel)
          {
               if (this.IsResubmit(vModel)) //  << Check Resubmit
               {
                   ViewBag.ErrorMsg = "Form is Resubmitting";
               }
               else
               {
                  // .... Post codes here without any changes...
               }
          
               this.PreventResubmit(vModel);// << Fill TempData & ViewModel PreventResubmit Property
          
               return View(vModel)
           }
          

          在视图中:

          @if (!string.IsNullOrEmpty(ViewBag.ErrorMsg))
          {
              <div>ViewBag.ErrorMsg</div>
          }
          
          @using (Html.BeginForm(...)){
          
              @Html.HiddenFor(x=>x.PreventResubmit) // << Put this Hidden Field in the form
          
              // Others codes of the form without any changes
          }
          

          在视图模型中:

          public class OrderViewModel: NoResubmitAbstract // << Inherit from NoResubmitAbstract 
          {
               // Without any changes!
          }
          

          你怎么看?


          我通过编写 2 个类使其变得简单:

          • NoResubmitAbstract抽象类
          • ControllerExtentions 静态类(System.Web.Mvc.ControllerBase 的扩展类)

          控制器扩展:

          public static class ControllerExtentions
          {
              [NonAction]
              public static bool IsResubmit (this System.Web.Mvc.ControllerBase controller, NoResubmitAbstract vModel)
              {
                  return (Guid)controller.TempData["PreventResubmit"]!= vModel.PreventResubmit;
              }
          
              [NonAction]
              public static void PreventResubmit(this System.Web.Mvc.ControllerBase controller, params NoResubmitAbstract[] vModels)
              {
                  var preventResubmitGuid = Guid.NewGuid();
                  controller.TempData["PreventResubmit"] = preventResubmitGuid ;
                  foreach (var vm in vModels)
                  {
                      vm.SetPreventResubmit(preventResubmitGuid);
                  }
              }
          }
          

          不重新提交摘要:

          public abstract class NoResubmitAbstract
          {
              public Guid PreventResubmit { get; set; }
          
              public void SetPreventResubmit(Guid prs)
              {
                  PreventResubmit = prs;
              }
          }
          

          只需将它们放入您的 MVC 项目并运行它... ;)

          【讨论】:

            【解决方案8】:

            在我的脑海中,在页面的 GET 请求的隐藏字段中生成 System.Guid 并将其与您的结帐/付款相关联。只需检查它并显示一条消息“付款已处理”。之类的。

            【讨论】:

              【解决方案9】:

              Kazi Manzur Ra​​shid 写了关于 this 的文章(连同其他 asp.net mvc 最佳实践)。他建议使用两个过滤器来处理 POST 和使用 TempData 的后续 GET 之间的数据传输。

              【讨论】:

                猜你喜欢
                • 2013-05-24
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多