服务器端处理重复的POST请求

本文关键字:POST 请求 处理 服务器端 | 更新日期: 2023-09-27 18:05:56

TL;DR:我想实现这个行为:

  1. 我用自定义修饰控制器中的特定POST动作属性,它只指定一个自定义int形参,以秒为单位,like [PreventSpam(Seconds=10)] =>来自同一客户端的两个请求被认为是"重复",如果POST对象是相同的两者之间的时间间隔小于"秒"
  2. 处理请求时,如果检测到"Duplicate"(根据之前的定义)它不应该被加工。的ActionResult(无论它是什么,视图,JSON…应该返回先前的、不重复的请求。当然了意味着ActionResult应该被第一个缓存请求。

如果我能够实现这一点,我就再也不用担心用户在提交请求时执行多次点击,从而重复请求,而不必通过javascript手动处理。

我在MVC 5中遇到了以下问题:当用户快速单击表单的提交按钮时,会生成多个相同的POST请求。

为了安全起见,我们希望在服务器端处理这个问题(如果我只通过javascript禁用提交按钮,没有人可以阻止恶意用户提交多个请求…)

我想实现以下结果:当多个,相同的请求被检测到一个特定的动作(让我们说,它们之间的时间跨度小于10秒),只有第一个请求被考虑-> 所有以下请求应该指向完全相同的"ActionResult"由第一个请求生成,应该以某种方式被缓存,这样的动作实际上只执行一次

从本指南(http://rion.io/2013/02/24/prevent-repeated-requests-using-actionfilters-in-asp-net-mvc/)中获得灵感,它实现了类似的功能(如果检测到多个请求,则添加一个模型状态错误…),我提出了以下代码:

public class PreventSpamAttribute : ActionFilterAttribute
{
    //This stores the time between Requests (in seconds)
    public int DelayRequest = 10;
    //THIS IS CALLED *AFTER* THE FIRST ACTION HAS BEEN PROPERLY EXECUTED -> THE ACTIONRESULT OBJECT HAS BEEN DETERMINED
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        var cache = filterContext.HttpContext.Cache;
        //I CACHE THE ACTION RESULT FOR "DelayRequest" SECONDS, USING THE GENERATED HASH AS KEY
        cache.Add(_getHash(filterContext.HttpContext), filterContext.Result, null, DateTime.Now.AddSeconds(DelayRequest), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
        base.OnResultExecuted(filterContext);
    }
    //THIS IS CALLED *BEFORE* CALLING THE ACTION
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var cache = filterContext.HttpContext.Cache;
        var hash = _getHash(filterContext.HttpContext);
        //IF I ALREADY HAVE A CACHED RESULT IT MEANS THAT THE USER CLICKED MULTIPLE TIMES,
        //INSTEAD OF CALLING THE ACTION I SIMPLY RETURN THE ACTIONRESULT THAT I CACHED..
        if (cache[hash] != null)            
            filterContext.Result = (ActionResult)cache[hash];
        base.OnActionExecuting(filterContext);
    }
    //GENERATES UNIQUE HASH, CONSIDERING VARIOUS REQUEST PARAMETERS
    private string _getHash(HttpContextBase httpContext)
    {
        //Store our HttpContext (for easier reference and code brevity)
        var request = httpContext.Request;
        //Store our HttpContext.Cache (for easier reference and code brevity)
        var cache = httpContext.Cache;
        //Grab the IP Address from the originating Request (very simple implementation for example purposes)
        var originationInfo = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;
        //Append the User Agent
        originationInfo += request.UserAgent;
        //Now we just need the target URL Information
        var targetInfo = request.RawUrl + request.QueryString;
        //Generate a hash for your strings (this appends each of the bytes of the value into a single hashed string
        return string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(originationInfo + targetInfo)).Select(s => s.ToString("x2")));
    }
}

不幸的是,我觉得这种方法有一个主要问题:如果这个动作需要一些时间来生成ActionResult怎么办?当重复的请求到达服务器时,另一个线程可能仍在处理第一个请求,因此ActionResult尚未被缓存。所以请求被再次处理……即使动作本身是同步的,据我所知,如果有多个同时请求到同一动作,线程池中的不同线程将被分配不同的请求,以便它们可以同时执行。这是正确的吗?

我的最终目标:处理所有服务器端操作,即使用户多次按下表单中的提交按钮,也应该和按一次没有什么不同

更新07/09/2016:在这个场景中,在每个表单的隐藏字段中存储一个令牌并不实际:在我们的几个视图中,我们通过javascript手动调用post操作,如下所示:

myUrl = '@Url.Action("MyAction","MyController")';
myPostObject = {
   entityId: 15,
   someValue: "aValue",
}
$.post(myUrl, myPostObject, callbackFunction);

必须手动编辑每个"myPostObject"包含一个额外的GUID字段将非常耗时和容易出错。这就是为什么我们正在寻找一个完全是服务器端的解决方案,只需要用自定义属性来修饰相关的操作。

服务器端处理重复的POST请求

我还建议这里的概念有一个主要缺陷——如果您的站点位于网络负载均衡器后面,那么任何MVC代码都无法告诉它已经在另一台服务器或IIS实例上收到了相同的请求。

如果线程处于负载状态,IIS将很高兴地启动另一个线程(在WebGarden模式下),因此您的单个应用程序实例永远无法可靠地判断用户是否使用了重复post攻击。

最终,您的代码将调用到某个地方的阴影状态服务器(通常是数据库,但也可能是共享内存缓存),这时您可以判断用户是否已经提交了请求。