Asp.net mvc 如何在不使用Javascript的情况下防止.NETMVC中的多表单提交?
我想防止用户在.NETMVC中多次提交表单。我尝试了几种使用Javascript的方法,但在所有浏览器中都很难使用。那么,如何在控制器中防止这种情况?是否有某种方法可以检测到多个提交?在其自身中,没有,但是根据控制器实际执行的操作,您应该能够找到一种方法 是否正在数据库中创建一条记录,您可以检查该记录是否已提交表单 我尝试了几种使用Javascript的方法,但在所有浏览器中都很难使用 你试过使用吗Asp.net mvc 如何在不使用Javascript的情况下防止.NETMVC中的多表单提交?,asp.net-mvc,Asp.net Mvc,我想防止用户在.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);
}
}