欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ASP.NET MVC5 频率控制Filter

程序员文章站 2024-02-28 09:33:58
...

类库项目类图:

ASP.NET MVC5 频率控制Filter

核心类:

ThrottlingFilter.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace MvcThrottle
{
    public class ThrottlingFilter : ActionFilterAttribute, IActionFilter
    {
        /// <summary>
        ///创建一个新的实例<see cref="ThrottlingHandler"/> 类.
        /// 默认情况下,<see cref="QuotaExceededResponseCode"/> 属性设置为429(太多请求).
        /// </summary>
        public ThrottlingFilter()
        {
            QuotaExceededResponseCode = (HttpStatusCode)429;
            Repository = new CacheRepository();
            IpAddressParser = new IpAddressParser();
        }

        /// <summary>
        /// 频率速率限制策略
        /// </summary>
        public ThrottlePolicy Policy { get; set; }

        /// <summary>
        ///频率指标存储
        /// </summary>
        public IThrottleRepository Repository { get; set; }

        /// <summary>
        ///记录阻塞的请求
        /// </summary>
        public IThrottleLogger Logger { get; set; }

        /// <summary>
        ///如果没有指定,默认值为:HTTP请求超出配额!每{1}最多允许{0}
        /// </summary>
        public string QuotaExceededMessage { get; set; }

        /// <summary>
        /// 获取或设置值作为HTTP状态代码返回,因为由于限制策略拒绝请求。 默认值为429(太多请求)。
        /// </summary>
        public HttpStatusCode QuotaExceededResponseCode { get; set; }

        public IIpAddressParser IpAddressParser { get; set; }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            EnableThrottlingAttribute attrPolicy = null;
            var applyThrottling = ApplyThrottling(filterContext, out attrPolicy);

            if (Policy != null && applyThrottling)
            {
                var identity = SetIdentity(filterContext.HttpContext.Request);

                if (!IsWhitelisted(identity))
                {
                    TimeSpan timeSpan = TimeSpan.FromSeconds(1);

                    var rates = Policy.Rates.AsEnumerable();
                    if (Policy.StackBlockedRequests)
                    {
                        //所有请求(包括拒绝的请求)将按照以下顺序进行堆叠:天,时,分,秒,如果客户端遇到小时限制,则分钟和秒计数器将会过期并最终从缓存中擦除
                        rates = Policy.Rates.Reverse();
                    }

                    //应用策略
                    //最后应用IP规则,并覆盖您可能定义的任何客户端规则
                    foreach (var rate in rates)
                    {
                        var rateLimitPeriod = rate.Key;
                        var rateLimit = rate.Value;

                        switch (rateLimitPeriod)
                        {
                            case RateLimitPeriod.Second:
                                timeSpan = TimeSpan.FromSeconds(1);
                                break;
                            case RateLimitPeriod.Minute:
                                timeSpan = TimeSpan.FromMinutes(1);
                                break;
                            case RateLimitPeriod.Hour:
                                timeSpan = TimeSpan.FromHours(1);
                                break;
                            case RateLimitPeriod.Day:
                                timeSpan = TimeSpan.FromDays(1);
                                break;
                            case RateLimitPeriod.Week:
                                timeSpan = TimeSpan.FromDays(7);
                                break;
                        }

                        //增量计数器
                        string requestId;
                        var throttleCounter = ProcessRequest(identity, timeSpan, rateLimitPeriod, out requestId);

                        if (throttleCounter.Timestamp + timeSpan < DateTime.UtcNow)
                            continue;

                        //应用EnableThrottlingAttribute策略
                        var attrLimit = attrPolicy.GetLimit(rateLimitPeriod);
                        if (attrLimit > 0)
                        {
                            rateLimit = attrLimit;
                        }

                        //应用终点速率限制
                        if (Policy.EndpointRules != null)
                        {
                            var rules = Policy.EndpointRules.Where(x => identity.Endpoint.IndexOf(x.Key, 0, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
                            if (rules.Any())
                            {
                                //从所有应用规则获得下限
                                var customRate = (from r in rules let rateValue = r.Value.GetLimit(rateLimitPeriod) select rateValue).Min();

                                if (customRate > 0)
                                {
                                    rateLimit = customRate;
                                }
                            }
                        }


                        //应用自定义速率限制会覆盖客户端端点限制
                        if (Policy.ClientRules != null && Policy.ClientRules.Keys.Contains(identity.ClientKey))
                        {
                            var limit = Policy.ClientRules[identity.ClientKey].GetLimit(rateLimitPeriod);
                            if (limit > 0) rateLimit = limit;
                        }

                        //应用user agent的自定义速率限制
                        if (Policy.UserAgentRules != null && !string.IsNullOrEmpty(identity.UserAgent))
                        {
                            var rules = Policy.UserAgentRules.Where(x => identity.UserAgent.IndexOf(x.Key, 0, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
                            if (rules.Any())
                            {

                                //从所有应用规则获得下限
                                var customRate = (from r in rules let rateValue = r.Value.GetLimit(rateLimitPeriod) select rateValue).Min();
                                rateLimit = customRate;
                            }
                        }

                        //执行最大限度的IP 速率限制
                        string ipRule = null;
                        if (Policy.IpRules != null && IpAddressParser.ContainsIp(Policy.IpRules.Keys.ToList(), identity.ClientIp, out ipRule))
                        {
                            var limit = Policy.IpRules[ipRule].GetLimit(rateLimitPeriod);
                            if (limit > 0) rateLimit = limit;
                        }

                        //检查是否达到限制
                        if (rateLimit > 0 && throttleCounter.TotalRequests > rateLimit)
                        {
                            //日志记录阻塞请求
                            if (Logger != null) Logger.Log(ComputeLogEntry(requestId, identity, throttleCounter, rateLimitPeriod.ToString(), rateLimit, filterContext.HttpContext.Request));

                            //跳出执行并返回409
                            var message = string.IsNullOrEmpty(QuotaExceededMessage) ?
                                "HTTP请求配额超出!每{1}最多允许{0}次" : QuotaExceededMessage;

                            //添加状态代码,并在x秒后重试以进行响应
                            filterContext.HttpContext.Response.StatusCode = (int)QuotaExceededResponseCode;
                            filterContext.HttpContext.Response.Headers.Set("Retry-After", RetryAfterFrom(throttleCounter.Timestamp, rateLimitPeriod));

                            filterContext.Result = QuotaExceededResult(
                                filterContext.RequestContext,
                                string.Format(message, rateLimit, rateLimitPeriod),
                                QuotaExceededResponseCode,
                                requestId);

                            return;
                        }
                    }
                }
            }

            base.OnActionExecuting(filterContext);
        }

        protected virtual RequestIdentity SetIdentity(HttpRequestBase request)
        {
            var entry = new RequestIdentity();
            entry.ClientIp = IpAddressParser.GetClientIp(request).ToString();

            entry.ClientKey = request.IsAuthenticated ? "auth" : "anon";

            var rd = request.RequestContext.RouteData;
            string currentAction = rd.GetRequiredString("action");
            string currentController = rd.GetRequiredString("controller");

            switch (Policy.EndpointType)
            {
                case EndpointThrottlingType.AbsolutePath:
                    entry.Endpoint = request.Url.AbsolutePath;
                    break;
                case EndpointThrottlingType.PathAndQuery:
                    entry.Endpoint = request.Url.PathAndQuery;
                    break;
                case EndpointThrottlingType.ControllerAndAction:
                    entry.Endpoint = currentController + "/" + currentAction;
                    break;
                case EndpointThrottlingType.Controller:
                    entry.Endpoint = currentController;
                    break;
                default:
                    break;
            }

            //不区分路由大小写
            entry.Endpoint = entry.Endpoint.ToLowerInvariant();

            entry.UserAgent = request.UserAgent;

            return entry;
        }

        static readonly object _processLocker = new object();
        private ThrottleCounter ProcessRequest(RequestIdentity requestIdentity, TimeSpan timeSpan, RateLimitPeriod period, out string id)
        {
            var throttleCounter = new ThrottleCounter()
            {
                Timestamp = DateTime.UtcNow,
                TotalRequests = 1
            };

            id = ComputeThrottleKey(requestIdentity, period);

            //串行读写
            lock (_processLocker)
            {
                var entry = Repository.FirstOrDefault(id);
                if (entry.HasValue)
                {
                    //条目尚未过期
                    if (entry.Value.Timestamp + timeSpan >= DateTime.UtcNow)
                    {
                        //递增请求计数
                        var totalRequests = entry.Value.TotalRequests + 1;

                        //深拷贝
                        throttleCounter = new ThrottleCounter
                        {
                            Timestamp = entry.Value.Timestamp,
                            TotalRequests = totalRequests
                        };

                    }
                }

                //存储: id (string) - timestamp (datetime) - total (long)
                Repository.Save(id, throttleCounter, timeSpan);
            }

            return throttleCounter;
        }

        protected virtual string ComputeThrottleKey(RequestIdentity requestIdentity, RateLimitPeriod period)
        {
            var keyValues = new List<string>()
                {
                    "throttle"
                };

            if (Policy.IpThrottling)
                keyValues.Add(requestIdentity.ClientIp);

            if (Policy.ClientThrottling)
                keyValues.Add(requestIdentity.ClientKey);

            if (Policy.EndpointThrottling)
                keyValues.Add(requestIdentity.Endpoint);

            if (Policy.UserAgentThrottling)
                keyValues.Add(requestIdentity.UserAgent);

            keyValues.Add(period.ToString());

            var id = string.Join("_", keyValues);
            var idBytes = Encoding.UTF8.GetBytes(id);
            var hashBytes = new System.Security.Cryptography.SHA1Managed().ComputeHash(idBytes);
            var hex = BitConverter.ToString(hashBytes).Replace("-", "");
            return hex;
        }

        private string RetryAfterFrom(DateTime timestamp, RateLimitPeriod period)
        {
            var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
            var retryAfter = 1;
            switch (period)
            {
                case RateLimitPeriod.Minute:
                    retryAfter = 60;
                    break;
                case RateLimitPeriod.Hour:
                    retryAfter = 60 * 60;
                    break;
                case RateLimitPeriod.Day:
                    retryAfter = 60 * 60 * 24;
                    break;
                case RateLimitPeriod.Week:
                    retryAfter = 60 * 60 * 24 * 7;
                    break;
            }
            retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
            return retryAfter.ToString(CultureInfo.InvariantCulture);
        }

        private bool IsWhitelisted(RequestIdentity requestIdentity)
        {
            if (Policy.IpThrottling)
                if (Policy.IpWhitelist != null && IpAddressParser.ContainsIp(Policy.IpWhitelist, requestIdentity.ClientIp))
                    return true;

            if (Policy.ClientThrottling)
                if (Policy.ClientWhitelist != null && Policy.ClientWhitelist.Contains(requestIdentity.ClientKey))
                    return true;

            if (Policy.EndpointThrottling)
                if (Policy.EndpointWhitelist != null && 
                    Policy.EndpointWhitelist.Any(x => requestIdentity.Endpoint.IndexOf(x, 0, StringComparison.InvariantCultureIgnoreCase) != -1))
                    return true;

            if (Policy.UserAgentThrottling && requestIdentity.UserAgent != null)
                if (Policy.UserAgentWhitelist != null && 
                    Policy.UserAgentWhitelist.Any(x => requestIdentity.UserAgent.IndexOf(x, 0, StringComparison.InvariantCultureIgnoreCase) != -1))
                    return true;

            return false;
        }

        private bool ApplyThrottling(ActionExecutingContext filterContext, out EnableThrottlingAttribute attr)
        {
            var applyThrottling = false;
            attr = null;

            if (filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(EnableThrottlingAttribute), true))
            {
                attr = (EnableThrottlingAttribute)filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(EnableThrottlingAttribute), true).First();
                applyThrottling = true;
            }

            //在类上 禁用属性
            if (filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(DisableThrottlingAttribute), true))
            {
                applyThrottling = false;
            }

            if (filterContext.ActionDescriptor.IsDefined(typeof(EnableThrottlingAttribute), true))
            {
                attr = (EnableThrottlingAttribute)filterContext.ActionDescriptor.GetCustomAttributes(typeof(EnableThrottlingAttribute), true).First();
                applyThrottling = true;
            }

            //显式禁用
            if (filterContext.ActionDescriptor.IsDefined(typeof(DisableThrottlingAttribute), true))
            {
                applyThrottling = false;
            }

            return applyThrottling;
        }

        protected virtual ActionResult QuotaExceededResult(RequestContext filterContext, string message, HttpStatusCode responseCode, string requestId)
        {
            return new HttpStatusCodeResult(responseCode, message);
        }

        private ThrottleLogEntry ComputeLogEntry(string requestId, RequestIdentity identity, ThrottleCounter throttleCounter, string rateLimitPeriod, long rateLimit, HttpRequestBase request)
        {
            return new ThrottleLogEntry
            {
                ClientIp = identity.ClientIp,
                ClientKey = identity.ClientKey,
                Endpoint = identity.Endpoint,
                UserAgent = identity.UserAgent,
                LogDate = DateTime.UtcNow,
                RateLimit = rateLimit,
                RateLimitPeriod = rateLimitPeriod,
                RequestId = requestId,
                StartPeriod = throttleCounter.Timestamp,
                TotalRequests = throttleCounter.TotalRequests,
                Request = request
            };
        }
    }
}

Mvc项目

BaseController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Demo.Controllers
{
    [EnableThrottling]
    public class BaseController : Controller
    {

    }
}

BlogController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Demo.Controllers
{
    [DisableThrottling]
    public class BlogController : BaseController
    {
        public ActionResult Index()
        {
            ViewBag.Message = "博客没有限制.";

            return View();
        }

        [EnableThrottling(PerSecond = 2, PerMinute = 5)]
        public ActionResult Search()
        {
            ViewBag.Message = "搜索被限制.";

            return View();
        }
    }
}

HomeController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Demo.Controllers
{
    public class HomeController : BaseController
    {
        public ActionResult Index()
        {
            return View();
        }

        [EnableThrottling(PerSecond = 2, PerMinute = 5)]
        public ActionResult About()
        {
            ViewBag.Message = "你的应用描述页.";

            return View();
        }

        [DisableThrottling]
        public ActionResult Contact()
        {
            ViewBag.Message = "你的联系页.";

            return View();
        }
    }
}

Helpers文件夹:

MvcThrottleCustomFilter.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace Demo.Helpers
{
    public class MvcThrottleCustomFilter : ThrottlingFilter
    {
        protected override ActionResult QuotaExceededResult(RequestContext filterContext, string message, System.Net.HttpStatusCode responseCode, string requestId)
        {
            var rateLimitedView = new ViewResult
            {
                ViewName = "RateLimited"
            };
            rateLimitedView.ViewData["Message"] = message;

            return rateLimitedView;
        }
    }
}

NginxIpAddressParser.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Demo.Helpers
{
    public class NginxIpAddressParser : IpAddressParser
    {
        public override string GetClientIp(HttpRequestBase request)
        {
            var ipAddress = request.UserHostAddress;

            //从反向代理获取客户端IP
            //如果客户端使用了代理服务器,则利用HTTP_X_FORWARDED_FOR找到客户端IP地址
            var xForwardedFor = request.ServerVariables["HTTP_X_FORWARDED_FOR"];
            if (!string.IsNullOrEmpty(xForwardedFor))
            {
                // 搜索公共IP地址
                var publicForwardingIps = xForwardedFor.Split(',').Where(ip => !IsPrivateIpAddress(ip)).ToList();

                // 如果发现任何公共IP,则使用NGINX时返回第一个IP地址,否则返回用户主机地址
                return publicForwardingIps.Any() ? publicForwardingIps.First().Trim() : ipAddress;
            }

            return ipAddress;
        }
    }
}

FilterConfig.cs

using MvcThrottle.Demo.Helpers;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;

namespace Demo
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());

            var throttleFilter = new MvcThrottleCustomFilter
            {
                Policy = new MvcThrottle.ThrottlePolicy(perSecond: 2, perMinute: 10, perHour: 60 * 10, perDay: 600 * 10)
                {
                    //IPs范围
                    IpThrottling = true,
                    IpRules = new Dictionary<string, MvcThrottle.RateLimits>
                    { 
                        { "::1/10", new MvcThrottle.RateLimits { PerHour = 15 } },
                        { "192.168.2.1", new MvcThrottle.RateLimits { PerMinute = 30, PerHour = 30*60, PerDay = 30*60*24 } }
                    },
                    IpWhitelist = new List<string> 
                    {
                        //localhost
                        // "::1",
                        "127.0.0.1",
                        //局域网
                        "192.168.0.0 - 192.168.255.255",
                        //Googlebot 谷歌的网页抓取机器人,类似于中国的Baiduspider(百度蜘蛛)
                        //更新自 http://iplists.com/nw/google.txt                    
                        "64.68.1 - 64.68.255",
                        "64.68.0.1 - 64.68.255.255",
                        "64.233.0.1 - 64.233.255.255",
                        "66.249.1 - 66.249.255",
                        "66.249.0.1 - 66.249.255.255",                        
                        "209.85.0.1 - 209.85.255.255",
                        "209.185.1 - 209.185.255",
                        "216.239.1 - 216.239.255",
                        "216.239.0.1 - 216.239.255.255",
                        //Bingbot                 
                        "65.54.0.1 - 65.54.255.255",
                        "68.54.1 - 68.55.255",
                        "131.107.0.1 - 131.107.255.255",
                        "157.55.0.1 - 157.55.255.255",
                        "202.96.0.1 - 202.96.255.255",
                        "204.95.0.1 - 204.95.255.255",
                        "207.68.1 - 207.68.255",
                        "207.68.0.1 - 207.68.255.255",
                        "219.142.0.1 - 219.142.255.255",
                        //Yahoo - 更新自http://user-agent-string.info/list-of-ua/bot-detail?bot=Yahoo!
                        "67.195.0.1 - 67.195.255.255",
                        "72.30.0.1 - 72.30.255.255",
                        "74.6.0.1 - 74.6.255.255",
                        "98.137.0.1 - 98.137.255.255",
                        //Yandex - 更新自 http://user-agent-string.info/list-of-ua/bot-detail?bot=YandexBot
                        //Yandex在俄罗斯本地搜索引擎的市场份额已远超俄罗斯Google
                        "100.43.0.1 - 100.43.255.255",
                        "178.154.0.1 - 178.154.255.255",
                        "199.21.0.1 - 199.21.255.255",
                        "37.140.0.1 - 37.140.255.255",
                        "5.255.0.1 - 5.255.255.255",
                        "77.88.0.1 - 77.88.255.255",
                        "87.250.0.1 - 87.250.255.255",
                        "93.158.0.1 - 93.158.255.255",
                        "95.108.0.1 - 95.108.255.255",
                    },

                    //客户端范围
                    ClientThrottling = true,
                    //白名单认证客户端
                    ClientWhitelist = new List<string> { "auth" },

                    //请求路径范围
                    EndpointThrottling = true,
                    EndpointType = EndpointThrottlingType.AbsolutePath,
                    EndpointRules = new Dictionary<string, RateLimits>
                    { 
                        { "home/", new RateLimits { PerHour = 90 } },
                        { "Home/about", new RateLimits { PerHour = 30 } }
                    },

                    //用户代理范围
                    UserAgentThrottling = true,
                    UserAgentWhitelist = new List<string>
                    {
                        "Googlebot",
                        "Mediapartners-Google",
                        "AdsBot-Google",
                        "Bingbot",
                        "YandexBot",
                        "DuckDuckBot"
                    },
                    UserAgentRules = new Dictionary<string, RateLimits>
                    {
                        {"Facebot", new RateLimits { PerMinute = 1 }},
                        {"Sogou", new RateLimits { PerHour = 1 } }
                    }

                },
                IpAddressParser = new NginxIpAddressParser(),
                Logger = new MvcThrottleCustomLogger()
            };

            filters.Add(throttleFilter);
        }
    }
}

Blog视图文件夹

Index.cshtml

@{
    ViewBag.Title = "Index";
}
<h2>@ViewBag.Title.</h2>
<h3>@ViewBag.Message</h3>

<p>但 @Html.ActionLink("search", "Search") 是被限制了.</p>

<p>使用此区域提供其他信息.</p>

Search.cshtml

@{
    ViewBag.Title = "Search";
}
<h2>@ViewBag.Title.</h2>
<h3>@ViewBag.Message</h3>

<p>但 @Html.ActionLink("blog", "Index") 没有限制.</p>

<p>使用此区域提供其他信息.</p>

Home视图文件夹

Index.cshtml

@{
    ViewBag.Title = "Home Page";
}

<div class="jumbotron">
    <h1>ASP.NET MVC频率筛选器</h1>
    <p class="lead">重新加载这个页面几次看到MvcThrottle在action.</p>
    <p><a href="@Url.Action("Index","Home")" class="btn btn-primary btn-large">Reload &raquo;</a></p>
</div>

运行结果

ASP.NET MVC5 频率控制Filter

如果在1秒内按F5刷新浏览器首页超过2次
ASP.NET MVC5 频率控制Filter

如果在1分钟内按F5刷新浏览器首页超过10次
ASP.NET MVC5 频率控制Filter

如果在1分小时内按F5刷新浏览器首页超过15次
ASP.NET MVC5 频率控制Filter

Blog视图Index页没被限制
ASP.NET MVC5 频率控制Filter

Blog视图Search页被限制
ASP.NET MVC5 频率控制Filter