Asp.net mvc 如何在不使用Javascript的情况下防止.NETMVC中的多表单提交?

Asp.net mvc 如何在不使用Javascript的情况下防止.NETMVC中的多表单提交?,asp.net-mvc,Asp.net Mvc,我想防止用户在.NETMVC中多次提交表单。我尝试了几种使用Javascript的方法,但在所有浏览器中都很难使用。那么,如何在控制器中防止这种情况?是否有某种方法可以检测到多个提交?在其自身中,没有,但是根据控制器实际执行的操作,您应该能够找到一种方法 是否正在数据库中创建一条记录,您可以检查该记录是否已提交表单 我尝试了几种使用Javascript的方法,但在所有浏览器中都很难使用 你试过使用吗 这应该考虑到浏览器之间的差异。您还可以在隐藏字段中传递某种令牌,并在控制器中对此进行验证 或者在

我想防止用户在.NETMVC中多次提交表单。我尝试了几种使用Javascript的方法,但在所有浏览器中都很难使用。那么,如何在控制器中防止这种情况?是否有某种方法可以检测到多个提交?

在其自身中,没有,但是根据控制器实际执行的操作,您应该能够找到一种方法

是否正在数据库中创建一条记录,您可以检查该记录是否已提交表单

我尝试了几种使用Javascript的方法,但在所有浏览器中都很难使用

你试过使用吗


这应该考虑到浏览器之间的差异。

您还可以在隐藏字段中传递某种令牌,并在控制器中对此进行验证


或者在提交值后使用重定向。但是如果你充分利用ajax,这就很困难了。

你可以在表单post中包含一个隐藏(随机或计数器)值,控制器可以在“打开”列表或类似的东西中跟踪这些值;每次您的控制器发出表单时,它都会嵌入一个值,并跟踪该值,允许一次post使用它。

不要重新发明轮子:)

使用设计模式


在这里,您可以找到一个和一个答案,其中给出了如何在ASP.NET MVC中实现它的一些建议。

为了完成@Darin的答案,如果您想要处理客户端验证(如果表单有必填字段),您可以在禁用提交按钮之前检查是否存在输入验证错误:

$('#myform').submit(function () {
    if ($(this).find('.input-validation-error').length == 0) {
        $(this).find(':submit').attr('disabled', 'disabled');
    }
});
如果我们使用
$(this.valid()


这适用于所有浏览器

 document.onkeydown = function () {
        switch (event.keyCode) {
            case 116: //F5 button
                event.returnValue = false;
                event.keyCode = 0;
                return false;
            case 82: //R button
                if (event.ctrlKey) {
                    event.returnValue = false;
                    event.keyCode = 0;
                    return false;
                }
        }
    }

更新了ASP.NET核心MVC(.NET核心和.NET 5.0)的答案

更新说明:请记住ASP.NET核心是

我将像以前一样坚持使用影响最小的用例,其中您只修饰那些您特别希望防止重复请求的控制器操作。如果您希望在每个请求上运行此筛选器,或者希望使用异步,那么还有其他选项。有关更多详细信息,请参阅

新的表单标记帮助器现在自动包含AntiForgeryToken,因此您不再需要手动将其添加到视图中

创建一个新的
ActionFilterAttribute
,如本例所示。您可以使用它做许多其他事情,例如,包括时间延迟检查,以确保即使用户提供两个不同的令牌,他们也不会每分钟提交多次

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PreventDuplicateRequestAttribute : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext context) {
        if (context.HttpContext.Request.HasFormContentType && context.HttpContext.Request.Form.ContainsKey("__RequestVerificationToken")) {
            var currentToken = context.HttpContext.Request.Form["__RequestVerificationToken"].ToString();
            var lastToken = context.HttpContext.Session.GetString("LastProcessedToken");

            if (lastToken == currentToken) {
                context.ModelState.AddModelError(string.Empty, "Looks like you accidentally submitted the same form twice.");
            }
            else {
                context.HttpContext.Session.SetString("LastProcessedToken", currentToken);
            }
        }
    }
}
根据请求,我还编写了一个异步版本

下面是定制
PreventDuplicateRequest
属性的人为使用示例

[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public IActionResult Create(InputModel input) {
    if (ModelState.IsValid) {
        // ... do something with input

        return RedirectToAction(nameof(SomeAction));
    }

    // ... repopulate bad input model data into a fresh viewmodel

    return View(viewModel);
}
关于测试的一个注意事项:在浏览器中简单地回击不会使用相同的AntiForgeryToken。在速度更快的计算机上,您无法实际双击按钮两次,您将需要使用类似的工具使用相同的令牌多次重播您的请求

关于设置的注意事项:默认情况下,核心MVC没有启用会话。您需要将
Microsoft.AspNet.Session
包添加到项目中,并正确配置
Startup.cs
。请阅读更多详情

会话设置的简短版本为: 在
Startup.ConfigureServices()
中,您需要添加:

services.AddDistributedMemoryCache();
services.AddSession();
Startup.Configure()
中,您需要在app.UseMvc()之前添加(


ASP.NET MVC(.NET Framework 4.x)的原始答案

首先,确保您在表单上使用了AntiForgeryToken

然后,您可以创建自定义ActionFilter:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class PreventDuplicateRequestAttribute : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        if (HttpContext.Current.Request["__RequestVerificationToken"] == null)
            return;

        var currentToken = HttpContext.Current.Request["__RequestVerificationToken"].ToString();

        if (HttpContext.Current.Session["LastProcessedToken"] == null) {
            HttpContext.Current.Session["LastProcessedToken"] = currentToken;
            return;
        }

        lock (HttpContext.Current.Session["LastProcessedToken"]) {
            var lastToken = HttpContext.Current.Session["LastProcessedToken"].ToString();

            if (lastToken == currentToken) {
                filterContext.Controller.ViewData.ModelState.AddModelError("", "Looks like you accidentally tried to double post.");
                return;
            }

            HttpContext.Current.Session["LastProcessedToken"] = currentToken;
        }
    }
}
在你的控制器动作上你只是

[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public ActionResult CreatePost(InputModel input) {
   ...
}

您会注意到,这并不能完全阻止请求。相反,它会在modelstate中返回一个错误,因此当您的操作检查modelstate.IsValid时,它会发现它不是有效的,并且会在正常的错误处理中返回。

您可以通过创建某种特定于用户的静态条目标志,或者特定于您想要保护资源的任何方式。我使用ConcurrentDictionary跟踪入口。密钥基本上是我保护的资源的名称和用户ID。诀窍是找出如何在知道请求当前正在处理时阻止请求

public async Task<ActionResult> SlowAction()
{
    if(!CanEnterResource(nameof(SlowAction)) return new HttpStatusCodeResult(204);
    try
    {
        // Do slow process
        return new SlowProcessActionResult();
    }
    finally
    {
       ExitedResource(nameof(SlowAction));
    }
}
public异步任务SlowAction()
{
如果(!CanInterResource(nameof(SlowAction))返回新的HttpStatusCodeResult(204);
尝试
{
//放慢进程
返回新的SlowProcessActionResult();
}
最后
{
ExitedResource(名称(SlowAction));
}
}

返回204是对双击请求的响应,在浏览器端不会执行任何操作。当缓慢的过程完成时,浏览器将收到对原始请求的正确响应,并相应地执行操作。

使用Post/Redirect/Get设计模式

附言:
在我看来,Jim Yarbro的答案可能有一个基本缺陷,即存储在
HttpContext.Current.Session[“LastProcessedToken”]
中的
\uu RequestVerificationToken
将在提交第二个表单时被替换(比如从另一个浏览器窗口)。此时,可以重新提交第一个表单,而不会将其视为重复提交。要使建议的模型正常工作,是否需要
\uu RequestVerificationToken
的历史记录?这似乎不可行。

使用此简单的jquery输入字段,即使您有多个子模块,也会非常有效它以单一形式显示按钮

$('input[type=submit]').click(function () {
    var clickedBtn = $(this)
    setTimeout(function () {
        clickedBtn.attr('disabled', 'disabled');
    }, 1);
});

只需在页面末尾添加此代码。我正在使用“jquery-3.3.1.min.js”和“bootstrap 4.3.1”


$('form')。提交(函数(){
if($(this).valid()){
$(this.find(':submit').attr('disabled','disabled');
}
});
策略 事实上,这个问题需要几条攻击线
[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public ActionResult CreatePost(InputModel input) {
   ...
}
public async Task<ActionResult> SlowAction()
{
    if(!CanEnterResource(nameof(SlowAction)) return new HttpStatusCodeResult(204);
    try
    {
        // Do slow process
        return new SlowProcessActionResult();
    }
    finally
    {
       ExitedResource(nameof(SlowAction));
    }
}
$('input[type=submit]').click(function () {
    var clickedBtn = $(this)
    setTimeout(function () {
        clickedBtn.attr('disabled', 'disabled');
    }, 1);
});
<script type="text/javascript">
    $('form').submit(function () {
        if ($(this).valid()) {
            $(this).find(':submit').attr('disabled', 'disabled');
        }
    });
</script>
$submitButton = $('#submitButton');
$('#mainForm').data('validator').settings.submitHandler = function (form) {
  form.submit();
  $submitButton.prop('disabled', true);
};
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);

if (history.Contains(token))
{
    context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
    history.Add(token);
}
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;

// This class provides an attribute for controller actions that flags duplicate form submissions
// by adding a model error if the request's verification token has already been seen on a prior
// form submission.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PreventDuplicateFormSubmissionAttribute: ActionFilterAttribute
{
    const string TokenKey = "__RequestVerificationToken";
    const string HistoryKey = "RequestVerificationTokenHistory";
    const int HistoryCapacity = 5;

    const string DuplicateSubmissionErrorMessage =
        "Your request was received more than once (either due to a temporary problem with the network or a " +
        "double button press). Any submissions after the first one have been rejected, but the status of the " +
        "first one is unclear. It may or may not have succeeded. Please check elsewhere to verify that your " +
        "request had the intended effect. You may need to resubmit it.";

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        HttpRequest request = context.HttpContext.Request;

        if (request.HasFormContentType && request.Form.ContainsKey(TokenKey))
        {
            string token = request.Form[TokenKey].ToString();

            ISession session = context.HttpContext.Session;
            var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);

            if (history.Contains(token))
            {
                context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
            }
            else
            {
                history.Add(token);
                session.Put(HistoryKey, history);
            }
        }
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        HttpRequest request = context.HttpContext.Request;

        if (request.HasFormContentType && request.Form.ContainsKey(TokenKey))
        {
            string token = request.Form[TokenKey].ToString();

            ISession session = context.HttpContext.Session;
            await session.LoadAsync();
            var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);

            if (history.Contains(token))
            {
                context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
            }
            else
            {
                history.Add(token);
                session.Put(HistoryKey, history);
                await session.CommitAsync();
            }
            await next();
        }
    }
}
using System.Linq;

// This class stores the last x items in an array.  Adding a new item overwrites the oldest item
// if there is no more empty space.  For the purpose of being JSON-serializable, its data is
// stored via public properties and it has a parameterless constructor.
public class RotatingHistory<T>
{
    public T[] Items { get; set; }
    public int Index { get; set; }

    public RotatingHistory() {}

    public RotatingHistory(int capacity)
    {
        Items = new T[capacity];
    }

    public void Add(T item)
    {
        Items[Index] = item;
        Index = ++Index % Items.Length;
    }

    public bool Contains(T item)
    {
        return Items.Contains(item);
    }
}
using System.Text.Json;
using Microsoft.AspNetCore.Http;

// This class is for storing (serializable) objects in session storage and retrieving them from it.
public static class SessonExtensions
{
    public static void Put<T>(this ISession session, string key, T value) where T : class
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    public static T Get<T>(this ISession session, string key) where T : class
    {
        string s = session.GetString(key);
        return s == null ? null : JsonSerializer.Deserialize<T>(s);
    }
}