重新整理 .net core 周边阅读篇————AspNetCoreRateLimit 之规则[二]
前言
本文和上文息息相关。
https://www.cnblogs.com/aoximin/p/15315102.html
是紧接着上文invoke来书写的,那么现在来逐行分析invoke到底干了啥。
正文
invoke 是一个方法,那么其一般符合一个套路。
参数检查
参数转换与检查(可能有可能无)
核心处理
返回参数(包括返回void)
那么就按照这个套路来看吧。
先看1和2 步骤吧。
// check if rate limiting is enabled
if (_options == null)
{
await _next.Invoke(context);
return;
}
// compute identity from request
var identity = await ResolveIdentityAsync(context);
// check white list
if (_processor.IsWhitelisted(identity))
{
await _next.Invoke(context);
return;
}
上面检查是否配置为空,如果为空就将请求转到下一个中间件。
// compute identity from request
var identity = await ResolveIdentityAsync(context);
这个属于参数转换。
查看ResolveIdentityAsync:
public virtual async Task<ClientRequestIdentity> ResolveIdentityAsync(HttpContext httpContext)
{
string clientIp = null;
string clientId = null;
if (_config.ClientResolvers?.Any() == true)
{
foreach (var resolver in _config.ClientResolvers)
{
clientId = await resolver.ResolveClientAsync(httpContext);
if (!string.IsNullOrEmpty(clientId))
{
break;
}
}
}
if (_config.IpResolvers?.Any() == true)
{
foreach (var resolver in _config.IpResolvers)
{
clientIp = resolver.ResolveIp(httpContext);
if (!string.IsNullOrEmpty(clientIp))
{
break;
}
}
}
return new ClientRequestIdentity
{
ClientIp = clientIp,
Path = httpContext.Request.Path.ToString().ToLowerInvariant().TrimEnd('/'),
HttpVerb = httpContext.Request.Method.ToLowerInvariant(),
ClientId = clientId ?? "anon"
};
}
这种一般先看返回值的,因为其在前方法中起作用的是返回值。
return new ClientRequestIdentity
{
ClientIp = clientIp,
Path = httpContext.Request.Path.ToString().ToLowerInvariant().TrimEnd('/'),
HttpVerb = httpContext.Request.Method.ToLowerInvariant(),
ClientId = clientId ?? "anon"
};
从这里面可以得知,是通过context,获取了ClientIp、Path、HttpVerb、clientId。
那么前文说过,我们只看下ip部分,那么看下这个ClientIp 是如何获取的吧。
if (_config.IpResolvers?.Any() == true)
{
foreach (var resolver in _config.IpResolvers)
{
clientIp = resolver.ResolveIp(httpContext);
if (!string.IsNullOrEmpty(clientIp))
{
break;
}
}
}
前文提及过了。这里再提及一遍。
这个_config 是IRateLimitConfiguration。
然后我们注册了配置:
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
RateLimitConfiguration 中的IpResolvers:
public IList<IIpResolveContributor> IpResolvers { get; } = new List<IIpResolveContributor>();
中间件初始化的时候:
_config.RegisterResolvers();
调用了RateLimitConfiguration的RegisterResolvers:
public virtual void RegisterResolvers()
{
string clientIdHeader = GetClientIdHeader();
string realIpHeader = GetRealIp();
if (clientIdHeader != null)
{
ClientResolvers.Add(new ClientHeaderResolveContributor(clientIdHeader));
}
// the contributors are resolved in the order of their collection index
if (realIpHeader != null)
{
IpResolvers.Add(new IpHeaderResolveContributor(realIpHeader));
}
IpResolvers.Add(new IpConnectionResolveContributor());
}
这里IpResolvers 就添加了一些ip的获取方式,这个在上文中细讲了,这里就只说其功能。
那么会到invoke中来,对于ip 限制来说,限制获取了clientip、path、methodverb。
那么invoke对clientip进行了检查,查看是是否在白名单中。
// check white list
if (_processor.IsWhitelisted(identity))
{
await _next.Invoke(context);
return;
}
IsWhitelisted 方法:
public virtual bool IsWhitelisted(ClientRequestIdentity requestIdentity)
{
if (_options.ClientWhitelist != null && _options.ClientWhitelist.Contains(requestIdentity.ClientId))
{
return true;
}
if (_options.IpWhitelist != null && IpParser.ContainsIp(_options.IpWhitelist, requestIdentity.ClientIp))
{
return true;
}
if (_options.EndpointWhitelist != null && _options.EndpointWhitelist.Any())
{
string path = _options.EnableRegexRuleMatching ? $".+:{requestIdentity.Path}" : $"*:{requestIdentity.Path}";
if (_options.EndpointWhitelist.Any(x => $"{requestIdentity.HttpVerb}:{requestIdentity.Path}".IsUrlMatch(x, _options.EnableRegexRuleMatching)) ||
_options.EndpointWhitelist.Any(x => path.IsUrlMatch(x, _options.EnableRegexRuleMatching)))
return true;
}
return false;
}
关注一下这个:
if (_options.IpWhitelist != null && IpParser.ContainsIp(_options.IpWhitelist, requestIdentity.ClientIp))
这里是返回是否在白名单的,如果有兴趣可以看下ContainsIp,里面关于了ip6的处理,虽然现在ip6用的不多,但是可以看看,万一真的有用户用ip6呢。
接下来就看下核心处理逻辑:
var rules = await _processor.GetMatchingRulesAsync(identity, context.RequestAborted);
var rulesDict = new Dictionary<RateLimitRule, RateLimitCounter>();
foreach (var rule in rules)
{
// increment counter
var rateLimitCounter = await _processor.ProcessRequestAsync(identity, rule, context.RequestAborted);
if (rule.Limit > 0)
{
// check if key expired
if (rateLimitCounter.Timestamp + rule.PeriodTimespan.Value < DateTime.UtcNow)
{
continue;
}
// check if limit is reached
if (rateLimitCounter.Count > rule.Limit)
{
//compute retry after value
var retryAfter = rateLimitCounter.Timestamp.RetryAfterFrom(rule);
// log blocked request
LogBlockedRequest(context, identity, rateLimitCounter, rule);
if (_options.RequestBlockedBehaviorAsync != null)
{
await _options.RequestBlockedBehaviorAsync(context, identity, rateLimitCounter, rule);
}
if (!rule.MonitorMode)
{
// break execution
await ReturnQuotaExceededResponse(context, rule, retryAfter);
return;
}
}
}
// if limit is zero or less, block the request.
else
{
// log blocked request
LogBlockedRequest(context, identity, rateLimitCounter, rule);
if (_options.RequestBlockedBehaviorAsync != null)
{
await _options.RequestBlockedBehaviorAsync(context, identity, rateLimitCounter, rule);
}
if (!rule.MonitorMode)
{
// break execution (Int32 max used to represent infinity)
await ReturnQuotaExceededResponse(context, rule, int.MaxValue.ToString(System.Globalization.CultureInfo.InvariantCulture));
return;
}
}
rulesDict.Add(rule, rateLimitCounter);
}
先看核心功能要用到的参数:
var rules = await _processor.GetMatchingRulesAsync(identity, context.RequestAborted);
var rulesDict = new Dictionary<RateLimitRule, RateLimitCounter>();
看下GetMatchingRulesAsync:
public async Task<IEnumerable<RateLimitRule>> GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken = default)
{
var policies = await _policyStore.GetAsync($"{_options.IpPolicyPrefix}", cancellationToken);
var rules = new List<RateLimitRule>();
if (policies?.IpRules?.Any() == true)
{
// search for rules with IP intervals containing client IP
var matchPolicies = policies.IpRules.Where(r => IpParser.ContainsIp(r.Ip, identity.ClientIp));
foreach (var item in matchPolicies)
{
rules.AddRange(item.Rules);
}
}
return GetMatchingRules(identity, rules);
}
这个是先获取该ip是否是我们特殊ip处理的规则,然后通过GetMatchingRules 判断其是否符合规则。
GetMatchingRules 应该就是处理核心了。
protected virtual List<RateLimitRule> GetMatchingRules(ClientRequestIdentity identity, List<RateLimitRule> rules = null)
{
var limits = new List<RateLimitRule>();
if (rules?.Any() == true)
{
if (_options.EnableEndpointRateLimiting)
{
// search for rules with endpoints like "*" and "*:/matching_path"
string path = _options.EnableRegexRuleMatching ? $".+:{identity.Path}" : $"*:{identity.Path}";
var pathLimits = rules.Where(r => path.IsUrlMatch(r.Endpoint, _options.EnableRegexRuleMatching));
limits.AddRange(pathLimits);
// search for rules with endpoints like "matching_verb:/matching_path"
var verbLimits = rules.Where(r => $"{identity.HttpVerb}:{identity.Path}".IsUrlMatch(r.Endpoint, _options.EnableRegexRuleMatching));
limits.AddRange(verbLimits);
}
else
{
// ignore endpoint rules and search for global rules only
var genericLimits = rules.Where(r => r.Endpoint == "*");
limits.AddRange(genericLimits);
}
// get the most restrictive limit for each period
limits = limits.GroupBy(l => l.Period).Select(l => l.OrderBy(x => x.Limit)).Select(l => l.First()).ToList();
}
// search for matching general rules
if (_options.GeneralRules != null)
{
var matchingGeneralLimits = new List<RateLimitRule>();
if (_options.EnableEndpointRateLimiting)
{
// search for rules with endpoints like "*" and "*:/matching_path" in general rules
var pathLimits = _options.GeneralRules.Where(r => $"*:{identity.Path}".IsUrlMatch(r.Endpoint, _options.EnableRegexRuleMatching));
matchingGeneralLimits.AddRange(pathLimits);
// search for rules with endpoints like "matching_verb:/matching_path" in general rules
var verbLimits = _options.GeneralRules.Where(r => $"{identity.HttpVerb}:{identity.Path}".IsUrlMatch(r.Endpoint, _options.EnableRegexRuleMatching));
matchingGeneralLimits.AddRange(verbLimits);
}
else
{
//ignore endpoint rules and search for global rules in general rules
var genericLimits = _options.GeneralRules.Where(r => r.Endpoint == "*");
matchingGeneralLimits.AddRange(genericLimits);
}
// get the most restrictive general limit for each period
var generalLimits = matchingGeneralLimits
.GroupBy(l => l.Period)
.Select(l => l.OrderBy(x => x.Limit).ThenBy(x => x.Endpoint))
.Select(l => l.First())
.ToList();
foreach (var generalLimit in generalLimits)
{
// add general rule if no specific rule is declared for the specified period
if (!limits.Exists(l => l.Period == generalLimit.Period))
{
limits.Add(generalLimit);
}
}
}
foreach (var item in limits)
{
if (!item.PeriodTimespan.HasValue)
{
// parse period text into time spans
item.PeriodTimespan = item.Period.ToTimeSpan();
}
}
limits = limits.OrderBy(l => l.PeriodTimespan).ToList();
if (_options.StackBlockedRequests)
{
limits.Reverse();
}
return limits;
}
这样看起来代码挺多的,但是这种也说明水不水特别深,为什么这么说呢?因为这里面基本没有调用其他的方法,都是写基础逻辑处理。
因为有很多if,那么就通过if来分段看。
var limits = new List<RateLimitRule>();
if (rules?.Any() == true)
{
if (_options.EnableEndpointRateLimiting)
{
// search for rules with endpoints like "*" and "*:/matching_path"
string path = _options.EnableRegexRuleMatching ? $".+:{identity.Path}" : $"*:{identity.Path}";
var pathLimits = rules.Where(r => path.IsUrlMatch(r.Endpoint, _options.EnableRegexRuleMatching));
limits.AddRange(pathLimits);
// search for rules with endpoints like "matching_verb:/matching_path"
var verbLimits = rules.Where(r => $"{identity.HttpVerb}:{identity.Path}".IsUrlMatch(r.Endpoint, _options.EnableRegexRuleMatching));
limits.AddRange(verbLimits);
}
else
{
// ignore endpoint rules and search for global rules only
var genericLimits = rules.Where(r => r.Endpoint == "*");
limits.AddRange(genericLimits);
}
// get the most restrictive limit for each period
limits = limits.GroupBy(l => l.Period).Select(l => l.OrderBy(x => x.Limit)).Select(l => l.First()).ToList();
}
这个一段是对我们特殊ip规则的处理,然后发现里面的逻辑其实是围绕着_options.EnableEndpointRateLimiting展开的。
那么从文档中EnableEndpointRateLimiting是什么呢?
If EnableEndpointRateLimiting is set to false then the limits will apply globally and only the rules that have as endpoint * will apply. For example, if you set a limit of 5 calls per second, any HTTP call to any endpoint will count towards that limit.
If EnableEndpointRateLimiting is set to true, then the limits will apply for each endpoint as in {HTTP_Verb}{PATH}. For example if you set a limit of 5 calls per second for *:/api/values a client can call GET /api/values 5 times per second but also 5 times PUT /api/values.
这上面是说如果EnableEndpointRateLimiting 是false 的话,那么限制只用于端点为"*"的情况。举了一个例子:如果你设置了每秒访问5次,那么你访问任何端点都会被计数。
如果EnableEndpointRateLimiting设置为true,那么限制将适用于每个端点,如{HTTP_Verb}{PATH}。例如,如果你为*:/api/值设置了每秒5次调用的限制,客户端可以每秒5次调用GET /api/值,也可以5次调用PUT /api/值。
说白了就是是否可以设置访问特殊Endpoint的访问限制。
有了上面的文档解释,那么看着代码只要按照这思路去看就行。
接下来看下一个if:
// search for matching general rules
if (_options.GeneralRules != null)
{
var matchingGeneralLimits = new List<RateLimitRule>();
if (_options.EnableEndpointRateLimiting)
{
// search for rules with endpoints like "*" and "*:/matching_path" in general rules
var pathLimits = _options.GeneralRules.Where(r => $"*:{identity.Path}".IsUrlMatch(r.Endpoint, _options.EnableRegexRuleMatching));
matchingGeneralLimits.AddRange(pathLimits);
// search for rules with endpoints like "matching_verb:/matching_path" in general rules
var verbLimits = _options.GeneralRules.Where(r => $"{identity.HttpVerb}:{identity.Path}".IsUrlMatch(r.Endpoint, _options.EnableRegexRuleMatching));
matchingGeneralLimits.AddRange(verbLimits);
}
else
{
//ignore endpoint rules and search for global rules in general rules
var genericLimits = _options.GeneralRules.Where(r => r.Endpoint == "*");
matchingGeneralLimits.AddRange(genericLimits);
}
// get the most restrictive general limit for each period
var generalLimits = matchingGeneralLimits
.GroupBy(l => l.Period)
.Select(l => l.OrderBy(x => x.Limit).ThenBy(x => x.Endpoint))
.Select(l => l.First())
.ToList();
foreach (var generalLimit in generalLimits)
{
// add general rule if no specific rule is declared for the specified period
if (!limits.Exists(l => l.Period == generalLimit.Period))
{
limits.Add(generalLimit);
}
}
}
同样我们应该看:_options.GeneralRules。
GeneralRules 就是我们限定的规则,里面同样看的是Endpoint是否匹配。
然后看最后一段:
foreach (var item in limits)
{
if (!item.PeriodTimespan.HasValue)
{
// parse period text into time spans
item.PeriodTimespan = item.Period.ToTimeSpan();
}
}
limits = limits.OrderBy(l => l.PeriodTimespan).ToList();
if (_options.StackBlockedRequests)
{
limits.Reverse();
}
return limits;
上面for 循环就是将我们的时间字符串转换为timespan(时间区间),然后从小到大排序一下。
接下来就看下_options.StackBlockedRequests,还是那个老套路看到配置文件查文档。
If StackBlockedRequests is set to false, rejected calls are not added to the throttle counter. If a client makes 3 requests per second and you've set a limit of one call per second, other limits like per minute or per day counters will only record the first call, the one that wasn't blocked. If you want rejected requests to count towards the other limits, you'll have to set StackBlockedRequests to true.
我直接用自己的理解说哈,如果StackBlockedRequests 设置为false,如果被拒绝的请求将不会加入到计数中。如果一个客户端每秒3次请求,你设置了每秒请求一次。那么其他的限制像每分钟和每天的计数器将只没有被拒绝的记录一次。
如果想拒绝的请求请求进行计数,那么你应该设置StackBlockedRequests 为true。
这里面就是说白了,就是拒绝的请求是否进行计数。
当然在这里还没有涉及到计数,StackBlockedRequests为true是将时间区间,从大到小排序了,这将成为后面的一个关键。
这里可以进行一个大胆的猜测,StackBlockedRequests 为fale的情况下,limits 是根据PeriodTimespan 从小到大排序的,也就是说是秒 分 小时 天这样排序的。
根据正常逻辑一般是秒先达到阀值,那么可能计数逻辑就是进行for循环,然后如果到达了限制那么就进行request block,很巧妙的一种设计。
这里可能就有人问了,如果是分到达了限制,那么秒不还是进行计数了吗?
这是没有关系的,因为分里面包含了秒。这里其实解决的是这样的一个问题,比如我在1秒中内请求了60次,那么有59次是失败的,那么如果请求算60次的话,那么会达到每分钟60次的现在,那么这个用户在一分钟内无法请求,故而建议StackBlockedRequests 设置为false。
结
因为篇幅限制,下一节是关于如何计数的。
重新整理 .net core 周边阅读篇————AspNetCoreRateLimit 之规则[二]的更多相关文章
- 重新整理 .net core 周边阅读篇————AspNetCoreRateLimit[一]
前言 整理了一下.net core 一些常见的库的源码阅读,共32个库,记100余篇. 以下只是个人的源码阅读,如有错误或者思路不正确,望请指点. 正文 github 地址为: https://git ...
- 重新整理 .net core 实践篇—————仓储层的具体实现[二十七]
前言 简单整理一下仓储层. 正文 在共享层的基础建设类库中: /// <summary> /// 泛型仓储接口 /// </summary> /// <typeparam ...
- 重新整理 .net core 实践篇————配置应用[一]
前言 本来想整理到<<重新整理.net core 计1400篇>>里面去,但是后来一想,整理 .net core 实践篇 是偏于实践,故而分开. 因为是重新整理,那么就从配置开 ...
- 如何在Visual Studio 2017中使用C# 7+语法 构建NetCore应用框架之实战篇(二):BitAdminCore框架定位及架构 构建NetCore应用框架之实战篇系列 构建NetCore应用框架之实战篇(一):什么是框架,如何设计一个框架 NetCore入门篇:(十二)在IIS中部署Net Core程序
如何在Visual Studio 2017中使用C# 7+语法 前言 之前不知看过哪位前辈的博文有点印象C# 7控制台开始支持执行异步方法,然后闲来无事,搞着,搞着没搞出来,然后就写了这篇博文,不 ...
- .NET Core实战项目之CMS 第二章 入门篇-快速入门ASP.NET Core看这篇就够了
作者:依乐祝 原文链接:https://www.cnblogs.com/yilezhu/p/9985451.html 本来这篇只是想简单介绍下ASP.NET Core MVC项目的(毕竟要照顾到很多新 ...
- .NET Core CSharp初级篇 1-1
.NET Core CSharp初级篇 1-1 本节内容是对于C#基础类型的存储方式以及C#基础类型的理论介绍 基础数据类型介绍 例如以下这句话:"张三是一名程序员,今年15岁重50.3kg ...
- net core体系-web应用程序-4asp.net core2.0 项目实战(CMS)-第二章 入门篇-快速入门ASP.NET Core看这篇就够了
.NET Core实战项目之CMS 第二章 入门篇-快速入门ASP.NET Core看这篇就够了 原文链接:https://www.cnblogs.com/yilezhu/p/9985451.ht ...
- 重新整理 .net core 实践篇————依赖注入应用[二]
前言 这里介绍一下.net core的依赖注入框架,其中其代码原理在我的另一个整理<<重新整理 1400篇>>中已经写了,故而专门整理应用这一块. 以下只是个人整理,如有问题, ...
- NET Core CSharp初级篇 1-3面向对象
.NET Core CSharp初级篇 1-3 本节内容为面向对象初级教程 类 简介 面向对象是整个C#中最核心最有特色的一个模块了,它很好的诠释了程序与现实世界的联系. 面向对象的三大特征:继承.多 ...
随机推荐
- Commons-Collections(二)之map
BidiMap: 双重Map 使用双向映射,可以使用值查找键,并且可以使用键轻松查找值.(自然,它可以根绝key移除,也可以根据value移除) public interface BidiMap< ...
- 连接共享打印机失败错误代码0x80070035
局域网内共享打印机非常方便,但是在连接中经常遇到问题,其中出现错误代码0x80070035的概率非常之高! 1.必须确保有关打印功能的相关服务都处于自动启动状态,重点检查TCP/IP NetBIOS ...
- 如何用Mybatis逆向工程实现分页查询(更改生成的Example)
如何用Mybatis逆向工程实现分页查询 一个很简单的方法,如果要mysql实现分页查询的话,执行下述语句即可 select * from table limit (offset)5,(limit) ...
- Hibernate之持久化对象
时间:2017-1-19 23:00 --Hibernate持久化类的状态1.持久化类:就是一个实体类与数据库建立了映射.2.Hibernate为了方便管理持久化类,将持久化类分为了三种状态: ...
- linux系统下深度学习环境搭建和使用
作为一个AI工程师,对Linux的一些技能的掌握也能从一定层面反应工程师的资深水平. 要求1:基于SSH的远程访问(本篇文章) 能用一台笔记本电脑,远程登陆一台linux服务器 能随时使用笔记本电脑启 ...
- C++类和对象笔记
笔记参考C++视频课程 黑马C++ C++ 面向对象的三大特性:封装.继承.多态 目录 目录 目录 一.封装 1.1 封装的意义-属性和行为 1.2 struct和class的区别 1.3 成员属性设 ...
- vscode如何配置ts的lint,如何配置才能让eslint和prettier不冲突一键格式化代码(vue开发使用)
最近在使用ts,发觉tslint在vscode上使用很不方便,不如eslint一键格式化高效,就想着能不能配置下vscode让其像写js一样爽 这篇文章主要解决2个问题,第一个是如何让vscode使用 ...
- 机械硬盘换到SSD后系统引导报错代码0xc000000e
由于机械硬盘IO不够用,系统使用起来非常的缓慢,特意购买了新的SSD进行了替换.机械硬盘的IO在70左右,SSD的IO在1000-4000左右指普通消费SSD. 由于不想安装系统,就直接把机械硬盘的数 ...
- Java基础(三)——内部类
一.内部类 内部类(Inner Class)就是定义在一个类里面的类.与之对应,包含内部类的类被称为外部类.内部类可以用private修饰. 1.为什么要定义内部类?或者内部类的作用是什么? 内部类提 ...
- C++11多线程编程
1. 多线程编程 在进行桌面应用程序开发的时候, 假设应用程序在某些情况下需要处理比较复杂的逻辑, 如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作.这种情况下就需要使用多线程,其中一 ...