ASP.NET MVC5 频率控制Filter

xiaoxiao2021-02-28  120

类库项目类图:

核心类:

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 »</a></p> </div>

运行结果

如果在1秒内按F5刷新浏览器首页超过2次

如果在1分钟内按F5刷新浏览器首页超过10次

如果在1分小时内按F5刷新浏览器首页超过15次

Blog视图Index页没被限制

Blog视图Search页被限制

转载请注明原文地址: https://www.6miu.com/read-28682.html

最新回复(0)