Сначала убедитесь, что вы используете 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.IsValid
, то оно увидит, что это не так, и вернется с вашей обычной обработкой ошибок.
ОБНОВЛЕНИЕ: вот решение ASP.NET Core 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);
}
}
}
}
По запросу я также написал асинхронную версию , которую можно найти здесь .
Вот пример надуманного использования пользовательского атрибута 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. На более быстрых компьютерах, где вы не можете физически дважды щелкнуть кнопку дважды, вам понадобится инструмент, подобный Fiddler , чтобы повторить ваш запрос с одним и тем же токеном несколько раз.
Примечание по настройке: Core MVC не имеет сессий, включенных по умолчанию. Вам нужно будет добавить пакет Microsoft.AspNet.Session
в ваш проект и правильно настроить Startup.cs
. Пожалуйста, прочитайте эту статью для более подробной информации.
Краткая версия настройки сеанса:
В Startup.ConfigureServices()
необходимо добавить:
services.AddDistributedMemoryCache();
services.AddSession();
В Startup.Configure()
необходимо добавить ( до app.UseMvc()
!!):
app.UseSession();