How do I prevent multiple form submission in .NET MVC without using Javascript?

后端 未结 13 2078
情深已故
情深已故 2020-11-28 02:36

I want to prevent users submitting forms multiple times in .NET MVC. I\'ve tried several methods using Javascript but have had difficulties getting it to work in all browser

相关标签:
13条回答
  • 2020-11-28 03:00

    In its self, no, however depending on what the controller is actually doing, you should be able to work out a way.

    Is a record being created in the database that you can check for to see if they've already submitted the form?

    0 讨论(0)
  • 2020-11-28 03:03

    ASP.NET MVC (.NET Framework 4.x)

    First, make sure you're using the AntiForgeryToken on your form.

    Then you can make a custom 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;
            }
        }
    }
    

    And on your controller action you just...

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

    You'll notice this doesn't prevent the request altogether. Instead it returns an error in the modelstate, so when your action checks if ModelState.IsValid then it will see that it is not, and will return with your normal error handling.


    ASP.NET Core MVC (.NET Core & .NET 5.0)

    Update Note: Remember ASP.NET Core is still called "Core" in .NET 5.0.

    I'm going to stick to the least-impact use case like before, where you're only adorning those controller actions that you specifically want to prevent duplicate requests on. If you want to have this filter run on every request, or want to use async, there are other options. See this article for more details.

    The new form tag helper now automatically includes the AntiForgeryToken so you no longer need to manually add that to your view.

    Create a new ActionFilterAttribute like this example. You can do many additional things with this, for example including a time delay check to make sure that even if the user presents two different tokens, they aren't submitting multiple times per minute.

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

    By request, I also wrote an asynchronous version which can be found here.

    Here's a contrived usage example of the custom PreventDuplicateRequest attribute.

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

    A note on testing: simply hitting back in a browser does not use the same AntiForgeryToken. On faster computers where you can't physically double click the button twice, you'll need to use a tool like Fiddler to replay your request with the same token multiple times.

    A note on setup: Core MVC does not have sessions enabled by default. You'll need to add the Microsoft.AspNet.Session package to your project, and configure your Startup.cs properly. Please read this article for more details.

    Short version of Session setup is: In Startup.ConfigureServices() you need to add:

    services.AddDistributedMemoryCache();
    services.AddSession();
    

    In Startup.Configure() you need to add (before app.UseMvc() !!):

    app.UseSession();
    
    0 讨论(0)
  • 2020-11-28 03:04

    You could include a hidden (random or counter) value in the form post, a controller could track these values in an 'open' list or something similar; every time your controller hands out a form it embeds a value, which it tracks allowing one post use of it.

    0 讨论(0)
  • 2020-11-28 03:05

    Just add this code at the end of your page. I am using "jquery-3.3.1.min.js" and "bootstrap 4.3.1"

    <script type="text/javascript">
        $('form').submit(function () {
            if ($(this).valid()) {
                $(this).find(':submit').attr('disabled', 'disabled');
            }
        });
    </script>
    
    0 讨论(0)
  • 2020-11-28 03:05

    You can do this by creating some sort of static entry flag that is user specific, or specific to whatever way you want to protect the resource. I use a ConcurrentDictionary to track entrance. The key is basically the name of the resource I'm protecting combined with the User ID. The trick is figuring out how to block the request when you know it's currently processing.

    public async Task<ActionResult> SlowAction()
    {
        if(!CanEnterResource(nameof(SlowAction)) return new HttpStatusCodeResult(204);
        try
        {
            // Do slow process
            return new SlowProcessActionResult();
        }
        finally
        {
           ExitedResource(nameof(SlowAction));
        }
    }
    

    Returning a 204 is a response to the double-click request that will do nothing on the browser side. When the slow process is done, the browser will receive the correct response for the original request and act accordingly.

    0 讨论(0)
  • 2020-11-28 03:05

    Use this simple jquery input field and will work awesomely even if you have multiple submit buttons in a single form.

    $('input[type=submit]').click(function () {
        var clickedBtn = $(this)
        setTimeout(function () {
            clickedBtn.attr('disabled', 'disabled');
        }, 1);
    });
    
    0 讨论(0)
提交回复
热议问题