类库项目类图:
核心类:
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
{
public ThrottlingFilter()
{
QuotaExceededResponseCode = (HttpStatusCode)
429;
Repository =
new CacheRepository();
IpAddressParser =
new IpAddressParser();
}
public ThrottlePolicy Policy {
get;
set; }
public IThrottleRepository Repository {
get;
set; }
public IThrottleLogger Logger {
get;
set; }
public string QuotaExceededMessage {
get;
set; }
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();
}
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;
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;
}
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;
}
}
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));
var message =
string.IsNullOrEmpty(QuotaExceededMessage) ?
"HTTP请求配额超出!每{1}最多允许{0}次" : QuotaExceededMessage;
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
};
}
}
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;
var xForwardedFor = request.ServerVariables[
"HTTP_X_FORWARDED_FOR"];
if (!
string.IsNullOrEmpty(xForwardedFor))
{
var publicForwardingIps = xForwardedFor.Split(
',').Where(ip => !IsPrivateIpAddress(ip)).ToList();
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)
{
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>
{
"127.0.0.1",
"192.168.0.0 - 192.168.255.255",
"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",
"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",
"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",
"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页被限制