如何在Web Api中限制请求?

时间:2013-12-28 17:25:19

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

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

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) {...}

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

9 个答案:

答案 0 :(得分:48)

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

  1. 缓存不提供不同线程之间的互锁控制,因此可以同时处理多个请求,从而引入跳过限制的额外调用。
  2. 正在处理过滤器并且在游戏中处理得太晚了。在Web API管道中,因此在您决定不应处理请求之前,需要花费大量资源。应该使用DelegatingHandler,因为它可以设置为在Web API管道的开头运行,并在执行任何其他工作之前切断请求。
  3. Http缓存本身是新的运行时可能无法使用的依赖项,例如自托管选项。最好避免这种依赖。
  4. 上述示例中的缓存并不能保证它在调用之间的生存,因为它可能因内存压力而被删除,尤其是低优先级。
  5. 虽然问题不是太糟糕,但请将响应状态设置为“冲突”。似乎不是最好的选择。最好使用' 429 - 太多的请求'代替。
  6. 在实施限制时,还有许多问题和隐藏的障碍需要解决。有免费的开源选项。我建议您查看https://throttlewebapi.codeplex.com/,例如。

答案 1 :(得分:43)

您似乎对ASP.NET MVC控制器的动作过滤器和ASP.NET Web API控制器的动作过滤器感到困惑。这是两个完全不同的类:

您所显示的是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控制器操作中使用此属性。

答案 2 :(得分:28)

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使用。

答案 3 :(得分:4)

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

System.Web.Mvc

答案 4 :(得分:2)

我使用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;
}

答案 5 :(得分:1)

我的2美分是为参数的请求信息添加一些额外的“密钥”信息,以便允许来自同一IP的不同参数请求。

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

另外,我对'clientIP'的关注,是否有可能两个不同的用户使用相同的ISP具有相同的'clientIP'?如果是,那么我的一个客户就会被错误地限制。

答案 6 :(得分:0)

在.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;
        }
    }
    }
}

答案 7 :(得分:0)

对于 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;
        }
    }
}

答案 8 :(得分:0)

您可以使用此代码

[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);
    }
}