ASP.NET MVC5 频率控制Filter
程序员文章站
2024-02-28 09:33:58
...
类库项目类图:
核心类:
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页被限制
上一篇: Android自定义仿微信PopupWindow效果
下一篇: javaWeb自定义标签用法实例详解