我想阻止用户在.NET MVC中多次提交表单.我已经尝试了几种使用Javascript的方法,但很难在所有浏览器中使用它.那么,如何在我的控制器中防止这种情况呢?在某种程度上可以检测到多个提交?
Jim*_*bro 58
首先,确保您在表单上使用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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
在您的控制器操作上,您只需......
[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public ActionResult CreatePost(InputModel input) {
...
}
Run Code Online (Sandbox Code Playgroud)
您会注意到这并不能完全阻止请求.相反,它会在模型状态中返回错误,因此当您的操作检查时是否ModelState.IsValid会看到它不是,并将返回正常的错误处理.
更新:这是一个ASP.NET核心MVC解决方案
我将像以前一样坚持使用影响最小的用例,在这种情况下,您只需要装饰那些您特别希望阻止重复请求的控制器操作.如果您希望在每个请求上运行此过滤器,或者想要使用异步,则还有其他选项.有关详细信息,请参阅此文章.
新的表单标记助手现在自动包含AntiForgeryToken,因此您不再需要手动将其添加到视图中.
ActionFilterAttribute像这个例子一样创建一个新的.您可以使用此功能执行许多其他操作,例如包括延时检查,以确保即使用户提供两个不同的令牌,它们也不会每分钟多次提交.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class PreventDuplicateRequestAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext context) {
if (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);
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
根据要求,我还写了一个异步版本,可以在这里找到.
这是自定义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);
}
Run Code Online (Sandbox Code Playgroud)
关于测试的注意事项:只需在浏览器中回击就不会使用相同的AntiForgeryToken.在速度较快的计算机上,您无法在物理上双击按钮两次,您需要使用像Fiddler这样的工具多次使用相同的令牌重放您的请求.
关于设置的注意事项:Core MVC默认情况下没有启用会话.您需要将Microsoft.AspNet.Session包添加到项目中,并Startup.cs正确配置.请阅读这篇文章了解更多详情.
会话设置的简短版本是:Startup.ConfigureServices()您需要添加:
services.AddDistributedMemoryCache();
services.AddSession();
Run Code Online (Sandbox Code Playgroud)
在Startup.Configure()你需要添加(之前 app.UseMvc() !!):
app.UseSession();
Run Code Online (Sandbox Code Playgroud)
Dar*_*rov 36
我已经尝试了几种使用Javascript的方法,但很难在所有浏览器中使用它
你尝试过使用jquery吗?
$('#myform').submit(function() {
$(this).find(':submit').attr('disabled', 'disabled');
});
Run Code Online (Sandbox Code Playgroud)
这应该照顾浏览器的差异.
Vin*_*nyG 30
只是为了完成@Darin的答案,如果你想处理客户端验证(如果表单有必填字段),你可以在禁用提交按钮之前检查是否有输入验证错误:
$('#myform').submit(function () {
if ($(this).find('.input-validation-error').length == 0) {
$(this).find(':submit').attr('disabled', 'disabled');
}
});
Run Code Online (Sandbox Code Playgroud)
aam*_*jad 10
如果我们使用$(this).valid()怎么办?
$('form').submit(function () {
if ($(this).valid()) {
$(this).find(':submit').attr('disabled', 'disabled');
}
});
Run Code Online (Sandbox Code Playgroud)
事实上,您需要采取多条路线来解决这个问题:
我不打算讨论 PRG,但这是我对其他两个主题的回答。他们建立在此处的其他答案之上。仅供参考,我正在使用 .NET Core 3.1。
假设您正在使用 jQuery 验证,我相信这是防止表单提交按钮被双击的最干净/最有效的方法。请注意,submitHandler仅在验证通过后调用,因此无需重新验证。
$submitButton = $('#submitButton');
$('#mainForm').data('validator').settings.submitHandler = function (form) {
form.submit();
$submitButton.prop('disabled', true);
};
Run Code Online (Sandbox Code Playgroud)
禁用提交按钮的另一种方法是在提交期间在表单前面显示覆盖层,以 1) 阻止与表单的任何进一步交互,2) 传达页面正在“做某事”。请参阅这篇文章了解更多详细信息。
我从上面吉姆·亚布罗(Jim Yarbro)的精彩回答开始,但后来我注意到马克·巴特勒(Mark Butler)的回答指出,如果有人通过多个浏览器选项卡提交表单,吉姆的方法会失败(因为每个选项卡都有不同的令牌,并且来自不同选项卡的帖子可以交错)。我确认确实存在这样的问题,然后决定从仅跟踪最后一个令牌升级为跟踪最后 x 个令牌。
为了实现这一点,我创建了几个帮助器类:一个用于存储最后 x 个令牌,另一个用于轻松地将对象存储到会话存储或从会话存储检索对象。主代码现在检查令牌历史记录中是否找不到当前令牌。除此之外,代码几乎相同。我只是做了一些小调整以适应我的口味。我包括了常规版本和异步版本。完整代码如下,但这些是关键行:
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
}
Run Code Online (Sandbox Code Playgroud)
遗憾的是,这种方法的致命缺陷是第一篇帖子(在任何重复之前)的反馈会丢失。更好(但更复杂)的解决方案是按 GUID 存储每个唯一请求的结果,然后通过不仅跳过再次执行工作而且从第一个请求返回相同结果来处理重复请求,从而为用户提供无缝体验。这篇详尽的文章详细介绍了爱彼迎避免重复付款的方法,将使您了解这些概念。
防止重复表单提交属性.cs
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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
旋转历史记录.cs
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);
}
}
Run Code Online (Sandbox Code Playgroud)
会话扩展.cs
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);
}
}
Run Code Online (Sandbox Code Playgroud)
不要重新发明轮子:)
使用Post/Redirect/Get设计模式.
在这里,您可以找到一个问题和答案,提供有关如何在ASP.NET MVC中实现它的一些建议.
| 归档时间: |
|
| 查看次数: |
36812 次 |
| 最近记录: |