【问题标题】:How to throttle requests in a Web Api?如何限制 Web Api 中的请求?
【发布时间】:2013-12-28 17:25:19
【问题描述】:

我正在尝试通过以下方式实现请求限制:

Best way to implement request throttling in ASP.NET MVC?

我已将该代码提取到我的解决方案中,并使用以下属性装饰了一个 API 控制器端点:

[Route("api/dothis/{id}")]
[AcceptVerbs("POST")]
[Throttle(Name = "TestThrottle", Message = "You must wait {n} seconds before accessing this url again.", Seconds = 5)]
[Authorize]
public HttpResponseMessage DoThis(int id) {...}

这可以编译,但属性的代码没有被命中,并且限制不起作用。我没有收到任何错误。我错过了什么?

【问题讨论】:

    标签: c# .net asp.net-web-api throttling


    【解决方案1】:

    建议的解决方案不准确。至少有 5 个原因。

    1. 缓存不提供不同线程之间的互锁控制,因此可以同时处理多个请求,从而引入额外的调用跳过节流。
    2. 在 Web API 管道中处理过滤器“为时已晚”,因此在您决定不处理该请求之前会花费大量资源。应该使用 DelegatingHandler,因为它可以设置为在 Web API 管道的开头运行并在执行任何其他工作之前切断请求。
    3. Http 缓存本身是新运行时可能不可用的依赖项,例如自托管选项。最好避免这种依赖。
    4. 上述示例中的缓存不保证其在调用之间的生存,因为它可能会由于内存压力而被删除,尤其是低优先级。
    5. 虽然这不是太糟糕的问题,但将响应状态设置为“冲突”似乎并不是最好的选择。最好改用“429-too many requests”。

    在实施节流时,还有更多问题和隐藏的障碍需要解决。有可用的免费开源选项。例如,我建议查看https://throttlewebapi.codeplex.com/

    【讨论】:

    • +1 表示不重新发明轮子。我目前正在评估nuget.org/packages/WebApiThrottle,看起来很有希望。
    • github.com/stefanprodan/WebApiThrottle 我相信是该项目的 github。我自己首先实现了公认的解决方案,学习它们很有趣,但是它们缺少很多想要的功能。没有理由重新发明轮子,这个模块是顶级的
    • 我已经按照 github 的说明实现了 webapithrottle,但它不适用于我的 web 服务中的任何 webmethods。除了在 webapiconfig 中设置通用节流处理程序之外,还有更多的实现吗?我还在 web 服务中添加了 enablethrottling。
    【解决方案2】:

    您似乎混淆了 ASP.NET MVC 控制器的操作过滤器和 ASP.NET Web API 控制器的操作过滤器。这是 2 个完全不同的类:

    您所展示的似乎是一个 Web API 控制器操作(在派生自 ApiController 的控制器内声明的操作)。因此,如果您想对其应用自定义过滤器,它们必须派生自 System.Web.Http.Filters.ActionFilterAttribute

    让我们继续为 Web API 修改代码:

    public class ThrottleAttribute : ActionFilterAttribute
    {
        /// <summary>
        /// A unique name for this Throttle.
        /// </summary>
        /// <remarks>
        /// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
        /// </remarks>
        public string Name { get; set; }
    
        /// <summary>
        /// The number of seconds clients must wait before executing this decorated route again.
        /// </summary>
        public int Seconds { get; set; }
    
        /// <summary>
        /// A text message that will be sent to the client upon throttling.  You can include the token {n} to
        /// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
        /// </summary>
        public string Message { get; set; }
    
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            var key = string.Concat(Name, "-", GetClientIp(actionContext.Request));
            var allowExecute = false;
    
            if (HttpRuntime.Cache[key] == null)
            {
                HttpRuntime.Cache.Add(key,
                    true, // is this the smallest data we can have?
                    null, // no dependencies
                    DateTime.Now.AddSeconds(Seconds), // absolute expiration
                    Cache.NoSlidingExpiration,
                    CacheItemPriority.Low,
                    null); // no callback
    
                allowExecute = true;
            }
    
            if (!allowExecute)
            {
                if (string.IsNullOrEmpty(Message))
                {
                    Message = "You may only perform this action every {n} seconds.";
                }
    
                actionContext.Response = actionContext.Request.CreateResponse(
                    HttpStatusCode.Conflict, 
                    Message.Replace("{n}", Seconds.ToString())
                );
            }
        }
    }
    

    GetClientIp 方法来自this post

    现在您可以在 Web API 控制器操作中使用此属性。

    【讨论】:

    • 太棒了!我相信我必须在GetClientIp 方法中将this.Request 更改为request,对吧?否则我会得到“无法解析符号”。这是一个巨大的帮助,非常感谢。
    • 应该是GetClientIp(actionContext.Request)
    • 对不起,我的意思是在方法定义中,而不是它的用法。引用代码中的第 10 行。
    【解决方案3】:

    WebApiThrottle 现在是该领域的佼佼者。

    它非常容易集成。只需将以下内容添加到App_Start\WebApiConfig.cs

    config.MessageHandlers.Add(new ThrottlingHandler()
    {
        // Generic rate limit applied to ALL APIs
        Policy = new ThrottlePolicy(perSecond: 1, perMinute: 20, perHour: 200)
        {
            IpThrottling = true,
            ClientThrottling = true,
            EndpointThrottling = true,
            EndpointRules = new Dictionary<string, RateLimits>
            { 
                 //Fine tune throttling per specific API here
                { "api/search", new RateLimits { PerSecond = 10, PerMinute = 100, PerHour = 1000 } }
            }
        },
        Repository = new CacheRepository()
    });
    

    它也可用作具有相同名称的 nuget。

    【讨论】:

    • 您对如何在 WebApiThrottling 中的 API 级别白名单 IP 有任何想法吗?
    • 点赞的爱@maheshsharma 在哪里? :) 对于您的问题,请查看github.com/stefanprodan/…
    • 我已经浏览了这个链接,但我不明白如何为特定的 api 制作它
    • 我认为不支持将每个 API 的 IP 列入白名单。您可以使用 API 密钥作为 IP 的替代品列入白名单吗? ClientWhitelist = new List&lt;string&gt; { "admin-key" }
    • 或使用自定义键 github.com/stefanprodan/… ClientRules = new Dictionary&lt;string, RateLimits&gt; { { "api-client-key-1", new RateLimits { PerMinute = 40, PerHour = 400 } }, { "api-client-key-9", new RateLimits { PerDay = 2000 } } }
    【解决方案4】:

    仔细检查您的操作过滤器中的 using 语句。当您使用 API 控制器时,请确保您引用的是 System.Web.Http.Filters 中的 ActionFilterAttribute 而不是 System.Web.Mvc 中的那个。

    using System.Web.Http.Filters;
    

    【讨论】:

    • 啊,行了。尽管这会引入很多错误,因为一切都取决于ActionExecutingContext,我认为现在需要HttpActionContext - 现在就解决它。谢谢!
    【解决方案5】:

    我正在使用ThrottleAttribute 来限制我的短消息发送API 的调用率,但我发现它有时不起作用。 API可能会被多次调用,直到油门逻辑起作用,最后我使用System.Web.Caching.MemoryCache而不是HttpRuntime.Cache,问题似乎解决了。

    if (MemoryCache.Default[key] == null)
    {
        MemoryCache.Default.Set(key, true, DateTime.Now.AddSeconds(Seconds));
        allowExecute = true;
    }
    

    【讨论】:

      【解决方案6】:

      我的 2 美分是为 'key' 添加一些关于参数请求信息的额外信息,以便允许来自同一 IP 的不同参数请求。

      key = Name + clientIP + actionContext.ActionArguments.Values.ToString()
      

      另外,我对“clientIP”有点担心,两个不同的用户使用同一个 ISP 是否有可能具有相同的“clientIP”?如果是,那么我可能会错误地限制一个客户端。

      【讨论】:

        【解决方案7】:

        对于.NET Core,您可以使用AspNetCoreRateLimit nuget package(这是来自WebApiThrottle 的同一开发人员的端口)。

        有一个有据可查的设置页面:https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/IpRateLimitMiddleware#setup

        【讨论】:

          【解决方案8】:

          在 .NET Core 中很容易解决。在这种情况下,我使用了 IMemoryCache,它是“每个服务在内存中”。但是,如果您希望它基于 Redis,例如只需将接口更改为 IDistributedCache...(当然要确保配置 Redis)

          using Microsoft.AspNetCore.Http;
          using Microsoft.AspNetCore.Mvc;
          using Microsoft.AspNetCore.Mvc.Filters;
          using Microsoft.Extensions.Caching.Distributed;
          using Microsoft.Extensions.Caching.Memory;
          using System;
          using System.Net;
          
          namespace My.ActionFilters
          {
              /// <summary>
              /// Decorates any MVC route that needs to have client requests limited by time.
              /// </summary>
              /// <remarks>
              /// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
              /// </remarks>
              [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
              public class ThrottleFilterAttribute : ActionFilterAttribute
              {
                  public ThrottleFilterAttribute()
                  {
          
                  }
                  /// <summary>
                  /// A unique name for this Throttle.
                  /// </summary>
                  /// <remarks>
                  /// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
                  /// </remarks>
                  public string Name { get; set; }
          
                  /// <summary>
                  /// The number of seconds clients must wait before executing this decorated route again.
                  /// </summary>
                  public int Seconds { get; set; }
          
                  /// <summary>
                  /// A text message that will be sent to the client upon throttling.  You can include the token {n} to
                  /// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
                  /// </summary>
                  public string Message { get; set; }
          
                  public override void OnActionExecuting(ActionExecutingContext c)
                  {
                       var memCache = (IMemoryCache)c.HttpContext.RequestServices.GetService(typeof(IMemoryCache));
                  var testProxy = c.HttpContext.Request.Headers.ContainsKey("X-Forwarded-For");
                  var key = 0;
                  if (testProxy)
                  {
                      var ipAddress = IPAddress.TryParse(c.HttpContext.Request.Headers["X-Forwarded-For"], out IPAddress realClient);
                      if (ipAddress)
                      {
                          key = realClient.GetHashCode(); 
                      }
                  }
                  if (key != 0)
                  {
                      key = c.HttpContext.Connection.RemoteIpAddress.GetHashCode();
                  }
                   memCache.TryGetValue(key, out bool forbidExecute);
          
                  memCache.Set(key, true, new MemoryCacheEntryOptions() { SlidingExpiration = TimeSpan.FromMilliseconds(Milliseconds) });
          
                  if (forbidExecute)
                  {
                      if (String.IsNullOrEmpty(Message))
                          Message = $"You may only perform this action every {Milliseconds}ms.";
          
                      c.Result = new ContentResult { Content = Message, ContentType = "text/plain" };
                      // see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
                      c.HttpContext.Response.StatusCode = StatusCodes.Status409Conflict;
                  }
              }
              }
          }
          

          【讨论】:

            【解决方案9】:

            对于 WebAPI 使用这个:

            using Microsoft.Owin;
            using System;
            using System.Net;
            using System.Net.Http;
            using System.Web;
            using System.Web.Caching;
            using System.Web.Http.Controllers;
            using System.Web.Http.Filters;
            
            namespace MyProject.Web.Resources
            {
                public enum TimeUnit
                {
                    Minute = 60,
                    Hour = 3600,
                    Day = 86400
                }
            
                [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
                public class ThrottleAttribute : ActionFilterAttribute
                {
                    public TimeUnit TimeUnit { get; set; }
                    public int Count { get; set; }
            
                    public override void OnActionExecuting(HttpActionContext filterContext)
                    {
                        var seconds = Convert.ToInt32(TimeUnit);
            
                        var key = string.Join(
                            "-",
                            seconds,
                            filterContext.Request.Method,
                            filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
                            filterContext.ActionDescriptor.ActionName,
                            GetClientIpAddress(filterContext.Request)
                        );
            
                        // increment the cache value
                        var cnt = 1;
                        if (HttpRuntime.Cache[key] != null)
                        {
                            cnt = (int)HttpRuntime.Cache[key] + 1;
                        }
                        HttpRuntime.Cache.Insert(
                            key,
                            cnt,
                            null,
                            DateTime.UtcNow.AddSeconds(seconds),
                            Cache.NoSlidingExpiration,
                            CacheItemPriority.Low,
                            null
                        );
            
                        if (cnt > Count)
                        {
                            filterContext.Response = new HttpResponseMessage
                            {
                                Content = new StringContent("You are allowed to make only " + Count + " requests per " + TimeUnit.ToString().ToLower())
                            };
                            filterContext.Response.StatusCode = (HttpStatusCode)429; //To Many Requests
                        }
                    }
            
                    private string GetClientIpAddress(HttpRequestMessage request)
                    {
                        if (request.Properties.ContainsKey("MS_HttpContext"))
                        {
                            return IPAddress.Parse(((HttpContextBase)request.Properties["MS_HttpContext"]).Request.UserHostAddress).ToString();
                        }
                        if (request.Properties.ContainsKey("MS_OwinContext"))
                        {
                            return IPAddress.Parse(((OwinContext)request.Properties["MS_OwinContext"]).Request.RemoteIpAddress).ToString();
                        }
                        return String.Empty;
                    }
                }
            }
            

            【讨论】:

              【解决方案10】:

              您可以使用此代码

              [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
              public class RateLimitAttribute : ActionFilterAttribute
              {
                  public int Seconds { get; set; }
              
                  public override void OnActionExecuting(HttpActionContext actionContext)
                  {
                      var key =
                          $"{actionContext.ActionDescriptor.ControllerDescriptor.ControllerName}-{actionContext.ActionDescriptor.ActionName}-{actionContext.ControllerContext.RequestContext.Principal.Identity.Name}";
                      var allowExecute = false;
              
                      if (HttpRuntime.Cache[key] == null)
                      {
                          HttpRuntime.Cache.Add(key,
                              true,
                              null,
                              DateTime.Now.AddSeconds(Seconds),
                              Cache.NoSlidingExpiration,
                              CacheItemPriority.Low,
                              null);
                          allowExecute = true;
                      }
              
                      if (!allowExecute)
                      {
                          actionContext.Response.Content = new StringContent($"سرویس های اسکنر را تنها می توانید هر {Seconds} استفاده کنید");
                          actionContext.Response.StatusCode = HttpStatusCode.Conflict;
                      }
              
                      base.OnActionExecuting(actionContext);
                  }
              }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2017-08-28
                • 1970-01-01
                • 2016-05-27
                • 1970-01-01
                • 2023-04-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多