13

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

PS:我看到了一些类似的问题:PHP:按下 Back 时阻止表单被意外重新处理以及如何阻止 Back 和 Refresh 按钮重新提交我的表单?但没有找到令人满意的答案......如果有这样的机制,ASP.NET MVC 特定的答案也将是理想的。

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

4

9 回答 9

21

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

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

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

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

这是问题的图形说明:

图表

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

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

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

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

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

这是一个示例控制器:

[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);
}
于 2015-02-09T20:11:00.873 回答
2

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

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

于 2010-01-21T19:35:22.467 回答
2

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

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

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

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

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

于 2016-05-05T16:57:15.257 回答
1

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

于 2010-01-21T19:35:34.910 回答
0

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

于 2010-01-21T19:37:50.307 回答
0

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

我使用一个TempData项目和一个Hidden field(表单中的一个属性)在两边( )ViewModel保持相同,并且我的标志是检测表单是否通过刷新重新提交。GuidServer/Client

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

行动:

[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 项目中并运行它... ;)

于 2017-06-13T02:22:29.893 回答
-1

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

于 2010-01-21T19:36:23.193 回答
-1

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

于 2010-01-22T13:38:02.410 回答