防止用户两次提交表单的好方法是什么?

时间:2022-12-01 09:04:22

I have a purchase page and I don't want the user to be able to refresh the page and resubmit the form once they get to the 'order complete' page because it automatically sets them up in our system via database values and charges their card via paypal (only want these to happen ONCE)... I have seen some sites that say 'Don't hit refresh or you will get charged twice!' but that is pretty lame to leave it open to possibility, what's a good way to only allow it to be submitted once or prevent them from refreshing, etc?

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

PS: I saw a few similar questions: PHP: Stop a Form from being accidentally reprocessed when Back is pressed and How do I stop the Back and Refresh buttons from resubmitting my form? but found no satisfactory answer... an ASP.NET MVC specific answer would be ideal too if there is a mechanism for this.

PS:我看到了一些类似的问题:PHP:当按下Back时停止表单被意外重新处理,如何停止重新提交表单后退和刷新按钮?但是没有找到令人满意的答案......如果有一个机制,ASP.NET MVC的具体答案也是理想的。

EDIT: Once they click submit it POSTS to my controller and then the controller does some magic and then returns a view with an order complete message, but if I click refresh on my browser it does the whole 'do you want to resend this form?' that is bad...

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

9 个解决方案

#1


18  

The standard solution to this is the POST/REDIRECT/GET pattern. This pattern can be implemented using pretty much any web development platform. You would typically:

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

  • Validate submission after POST
  • POST后验证提交
  • if it fails re-render the original entry form with validation errors displayed
  • 如果失败则重新呈现原始输入表单并显示验证错误
  • if it succeeds, REDIRECT to a confirmation page, or page where you re-display the input - this is the GET part
  • 如果成功,则重定向到确认页面或重新显示输入的页面 - 这是GET部分
  • since the last action was a GET, if the user refreshes at this point, there is no form re-submission to occur.
  • 由于最后一个操作是GET,如果用户此时刷新,则不会重新提交表单。

#2


13  

I 100% agree with RedFilter's generic answer, but wanted to post some relevant code for ASP.NET MVC specifically.

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

You can use the Post/Redirect/Get (PRG) Pattern to solve the double postback problem.

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

Here's an graphical illustration of the problem:

以下是问题的图解说明:

防止用户两次提交表单的好方法是什么?

What happens is when the user hits refresh, the browser attempts to resubmit the last request it made. If the last request was a post, the browser will attempt to do that.

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

Most browsers know that this isn't typically what the user wants to do, so will automatically ask:

大多数浏览器都知道这通常不是用户想要做的事情,因此会自动询问:

Chrome - The page that you're looking for used information that you entered. Returning to that page might cause any action you took to be repeated. Do you want to continue?
Firefox - To display this page, Firefox must send information that will repeat any action (such as a search or order confirmation) that was performed earlier.
Safari - Are you sure you want to send a form again? To reopen this page Safari must resend a form. This might result in duplicate purchases, comments, or other actions.
Internet Explorer - To display the webpage again, the web browser needs to resend the information you've previously submitted. If you were making a purchase, you should click Cancel to avoid a duplicate transaction. Otherwise, click Retry to display the webpage again.

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

But the PRG pattern helps avoid this altogether by sending the client a redirect message so when the page finally appears, the last request the browser executed was a GET request for the new resource.

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

Here's a great article on PRG that provides an implementation of the pattern for MVC. It's important to note that you only want to resort to a redirect when an non-idempotent action is performed on the server. In other words, if you have a valid model and have actually persisted the data in some way, then it's important to ensure the request isn't accidentally submitted twice. But if the model is invalid, the current page and model should be returned so the user can make any necessary modifications.

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

Here's an example Controller:

这是一个示例控制器:

[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


2  

While serving up the order confirmation page you can set a token that you also store in the DB/Cache. At the first instance of order confirmation, check for this token's existence and clear the token. If implemented with thread safety, you will not be able to submit the order twice.

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

This is just one of the many approaches possible.

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

#4


1  

Give each visitor's form a unique ID when the page is first loaded. Note the ID when the form is submitted. Once a form has been submitted with that ID, don't allow any further requests using it. If they click refresh, the same ID will be sent.

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

#5


1  

Note that the PRG pattern does not completely guard against multiple form submissions, as multiple post requests can be fired off even before a single redirect has taken place - this can lead to your form submissions not being idempotent.

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

Do take note of the answer that has been provided here, which provides a workaround to this issue, which I quote here for convenience:

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

If you make use of a hidden anti-forgery token in your form (as you should), you can cache the anti-forgery token on first submit and remove the token from cache if required, or expire the cached entry after set amount of time.

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

You will then be able to check with each request against the cache whether the specific form has been submitted and reject it if it has.

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

You don't need to generate your own GUID as this is already being done when generating the anti-forgery token.

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

#6


0  

Simply do a redirect from the page that does all the nasty stuff to the "Thank you for your order" page. Having done that, the user can hit refresh as many times as he likes.

只需从执行所有令人讨厌的内容的页面重定向到“感谢您的订单”页面。完成后,用户可以根据自己的喜好多次刷新。

#7


0  

If you doesn't like redirect the user to other page, then by using my way you dose not need Post/Redirect/Get (PRG) Pattern and the user remain on the current page without fear of the negative effects of re-submitting of the form!

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

I use a TempData item and a Hidden field (a property in the ViewModel of the form) to keep a same Guid in both sides (Server/Client) and it is my sign to detect if the form is Resubmitting by refresh or not.

我使用TempData项和隐藏字段(表单的ViewModel中的属性)在两侧保持相同的Guid(服务器/客户端),并且我的标志是检测表单是否通过刷新重新提交。

Final face of the codes looks like very short and simple:

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

Action:

行动:

[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)
 }

In View:

在视图中:

@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
}

In View Model:

在视图模型中:

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

What do you think?

你怎么看?


I make it simple by writing 2 class:

我通过编写2个类来简化:

  • NoResubmitAbstract abstract class
  • NoResubmitAbstract抽象类
  • ControllerExtentions static class (An Extension class for System.Web.Mvc.ControllerBase)
  • ControllerExtentions静态类(System.Web.Mvc.ControllerBase的Extension类)

ControllerExtentions:

ControllerExtentions:

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);
        }
    }
}

NoResubmitAbstract:

NoResubmitAbstract:

public abstract class NoResubmitAbstract
{
    public Guid PreventResubmit { get; set; }

    public void SetPreventResubmit(Guid prs)
    {
        PreventResubmit = prs;
    }
}

Just put them in your MVC project and run it... ;)

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

#8


-1  

Off the top of my head, generate a System.Guid in a hidden field on the GET request of the page and associate it with your checkout/payment. Simply check for it and display a message saying 'Payment already processed.' or such.

在我的头顶,在页面的GET请求的隐藏字段中生成System.Guid,并将其与您的结帐/付款相关联。只需检查并显示“已付款已付款”的消息。或者这样的。

#9


-1  

Kazi Manzur Rashid wrote about this (together with other asp.net mvc best-practices). He suggests using two filters to handle data transfer between the POST and the follwing GET using TempData.

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

#1


18  

The standard solution to this is the POST/REDIRECT/GET pattern. This pattern can be implemented using pretty much any web development platform. You would typically:

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

  • Validate submission after POST
  • POST后验证提交
  • if it fails re-render the original entry form with validation errors displayed
  • 如果失败则重新呈现原始输入表单并显示验证错误
  • if it succeeds, REDIRECT to a confirmation page, or page where you re-display the input - this is the GET part
  • 如果成功,则重定向到确认页面或重新显示输入的页面 - 这是GET部分
  • since the last action was a GET, if the user refreshes at this point, there is no form re-submission to occur.
  • 由于最后一个操作是GET,如果用户此时刷新,则不会重新提交表单。

#2


13  

I 100% agree with RedFilter's generic answer, but wanted to post some relevant code for ASP.NET MVC specifically.

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

You can use the Post/Redirect/Get (PRG) Pattern to solve the double postback problem.

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

Here's an graphical illustration of the problem:

以下是问题的图解说明:

防止用户两次提交表单的好方法是什么?

What happens is when the user hits refresh, the browser attempts to resubmit the last request it made. If the last request was a post, the browser will attempt to do that.

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

Most browsers know that this isn't typically what the user wants to do, so will automatically ask:

大多数浏览器都知道这通常不是用户想要做的事情,因此会自动询问:

Chrome - The page that you're looking for used information that you entered. Returning to that page might cause any action you took to be repeated. Do you want to continue?
Firefox - To display this page, Firefox must send information that will repeat any action (such as a search or order confirmation) that was performed earlier.
Safari - Are you sure you want to send a form again? To reopen this page Safari must resend a form. This might result in duplicate purchases, comments, or other actions.
Internet Explorer - To display the webpage again, the web browser needs to resend the information you've previously submitted. If you were making a purchase, you should click Cancel to avoid a duplicate transaction. Otherwise, click Retry to display the webpage again.

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

But the PRG pattern helps avoid this altogether by sending the client a redirect message so when the page finally appears, the last request the browser executed was a GET request for the new resource.

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

Here's a great article on PRG that provides an implementation of the pattern for MVC. It's important to note that you only want to resort to a redirect when an non-idempotent action is performed on the server. In other words, if you have a valid model and have actually persisted the data in some way, then it's important to ensure the request isn't accidentally submitted twice. But if the model is invalid, the current page and model should be returned so the user can make any necessary modifications.

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

Here's an example Controller:

这是一个示例控制器:

[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


2  

While serving up the order confirmation page you can set a token that you also store in the DB/Cache. At the first instance of order confirmation, check for this token's existence and clear the token. If implemented with thread safety, you will not be able to submit the order twice.

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

This is just one of the many approaches possible.

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

#4


1  

Give each visitor's form a unique ID when the page is first loaded. Note the ID when the form is submitted. Once a form has been submitted with that ID, don't allow any further requests using it. If they click refresh, the same ID will be sent.

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

#5


1  

Note that the PRG pattern does not completely guard against multiple form submissions, as multiple post requests can be fired off even before a single redirect has taken place - this can lead to your form submissions not being idempotent.

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

Do take note of the answer that has been provided here, which provides a workaround to this issue, which I quote here for convenience:

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

If you make use of a hidden anti-forgery token in your form (as you should), you can cache the anti-forgery token on first submit and remove the token from cache if required, or expire the cached entry after set amount of time.

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

You will then be able to check with each request against the cache whether the specific form has been submitted and reject it if it has.

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

You don't need to generate your own GUID as this is already being done when generating the anti-forgery token.

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

#6


0  

Simply do a redirect from the page that does all the nasty stuff to the "Thank you for your order" page. Having done that, the user can hit refresh as many times as he likes.

只需从执行所有令人讨厌的内容的页面重定向到“感谢您的订单”页面。完成后,用户可以根据自己的喜好多次刷新。

#7


0  

If you doesn't like redirect the user to other page, then by using my way you dose not need Post/Redirect/Get (PRG) Pattern and the user remain on the current page without fear of the negative effects of re-submitting of the form!

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

I use a TempData item and a Hidden field (a property in the ViewModel of the form) to keep a same Guid in both sides (Server/Client) and it is my sign to detect if the form is Resubmitting by refresh or not.

我使用TempData项和隐藏字段(表单的ViewModel中的属性)在两侧保持相同的Guid(服务器/客户端),并且我的标志是检测表单是否通过刷新重新提交。

Final face of the codes looks like very short and simple:

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

Action:

行动:

[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)
 }

In View:

在视图中:

@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
}

In View Model:

在视图模型中:

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

What do you think?

你怎么看?


I make it simple by writing 2 class:

我通过编写2个类来简化:

  • NoResubmitAbstract abstract class
  • NoResubmitAbstract抽象类
  • ControllerExtentions static class (An Extension class for System.Web.Mvc.ControllerBase)
  • ControllerExtentions静态类(System.Web.Mvc.ControllerBase的Extension类)

ControllerExtentions:

ControllerExtentions:

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);
        }
    }
}

NoResubmitAbstract:

NoResubmitAbstract:

public abstract class NoResubmitAbstract
{
    public Guid PreventResubmit { get; set; }

    public void SetPreventResubmit(Guid prs)
    {
        PreventResubmit = prs;
    }
}

Just put them in your MVC project and run it... ;)

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

#8


-1  

Off the top of my head, generate a System.Guid in a hidden field on the GET request of the page and associate it with your checkout/payment. Simply check for it and display a message saying 'Payment already processed.' or such.

在我的头顶,在页面的GET请求的隐藏字段中生成System.Guid,并将其与您的结帐/付款相关联。只需检查并显示“已付款已付款”的消息。或者这样的。

#9


-1  

Kazi Manzur Rashid wrote about this (together with other asp.net mvc best-practices). He suggests using two filters to handle data transfer between the POST and the follwing GET using TempData.

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