如何在Web Api中限制请求?

60

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

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

这段代码可以编译通过,但属性的代码没有被执行,并且限流也没有生效。虽然我没有收到任何错误提示,但是我错过了什么吗?

11个回答

64

提出的解决方案不准确。至少有五个原因。

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

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


10
+1 表示不要重复造轮子。我目前正在评估 https://www.nuget.org/packages/WebApiThrottle/,看起来很有前途。 - Tim Cools
1
我相信这是该项目的Github链接:https://github.com/stefanprodan/WebApiThrottle。我自己首先实现了已接受的解决方案,学习它们很有趣,但它们缺少许多想要的功能。没有必要重新发明轮子,这个模块非常优秀。 - Arijoon
我已按照GitHub的说明实现了WebApiThrottle,但它对于我的WebService中的任何WebMethod都不起作用。 实现是否还需要更多步骤,除了在WebApiConfig中设置通用的ThrottleHandler之外? 我还在WebService中添加了EnableThrottling。 - R Shavers

54

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列入白名单? - mahesh sharma
@maheshsharma,点赞在哪里呢? :) 关于你的问题,请查看https://github.com/stefanprodan/WebApiThrottle#global-throttling-based-on-ip - Korayem
我认为不支持按API白名单IP。您可以使用API密钥作为IP的替代进行白名单处理吗?ClientWhitelist = new List<string> { "admin-key" } - Korayem
这对我不起作用。我的 API 调用是位于 Web 服务内部的 Web 方法。有人可以分享一个完整的示例吗? - R Shavers
1
IpWhitelist = 新的 List<string> { "::1", "192.168.0.0/24" },-- > 用于 IP 白名单。 - Satish Patil
显示剩余2条评论

53

您似乎将ASP.NET MVC控制器的Action Filters和ASP.NET Web API控制器的Action Filters混淆了。这是两个完全不同的类:

看起来您展示的是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,是吗?否则我会得到“无法解析符号”的错误。这对我帮助很大,非常感谢。 - RobVious
应该是 GetClientIp(actionContext.Request) - Darin Dimitrov
抱歉,我是指在方法定义内部,而不是它的使用。参考代码中的第10行。 - RobVious

6

请仔细检查您操作筛选器中的using语句。由于您正在使用API控制器,请确保引用System.Web.Http.Filters中的ActionFilterAttribute,而不是System.Web.Mvc中的ActionFilterAttribute。

using System.Web.Http.Filters;

2
啊,是的,那就可以了。虽然这会引入很多错误,因为一切都依赖于“ActionExecutingContext”,但我现在相信它需要变成“HttpActionContext”——正在处理中。谢谢! - RobVious

3

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

3

2

我认为需要在“key”方面添加一些额外的信息,以便允许来自同一IP的不同参数请求。

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

另外,我有一个小疑问关于“clientIP”,如果使用相同ISP的两个不同用户具有相同的“clientIP”,是否可能导致一个客户端被错误地限制流量?


2
你可以使用这段代码。
[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);
    }
}

2

如果要使用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;
        }
    }
}

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接