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>

运行结果

这里写图片描述

如果在1秒内按F5刷新浏览器首页超过2次
这里写图片描述

如果在1分钟内按F5刷新浏览器首页超过10次
这里写图片描述

如果在1分小时内按F5刷新浏览器首页超过15次
这里写图片描述

Blog视图Index页没被限制
这里写图片描述

Blog视图Search页被限制
这里写图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值