重新整理 .net core 周邊閱讀篇————AspNetCoreRateLimit[一]
- 2021 年 9 月 24 日
- 筆記
- .net core(web)
前言
整理了一下.net core 一些常見的庫的源碼閱讀,共32個庫,記100餘篇。
以下只是個人的源碼閱讀,如有錯誤或者思路不正確,望請指點。
正文
github 地址為:
//github.com/stefanprodan/AspNetCoreRateLimit
一般個人習慣先閱讀readme的簡介。
上面大概翻譯是:
AspNetCoreRateLimit 是ASP.NET Core 訪問速率限制的解決方案,設計基於ip地址和客戶端id用於控制用於web api和 mvc app的客戶端訪問速率。
這個包包含了IpRateLimitMiddleware and a ClientRateLimitMiddleware兩個中間件,用這兩個中間件你根據不同的場景能設置幾種不同的限制,
比如限制一個客戶端或者一個ip在幾秒或者15分鐘內訪問最大限制。您可以定義這些限制來處理對某個API的所有請求,也可以將這些限制限定在指定範圍的每個API URL或HTTP請求路徑上。
上面說了這麼多就是用來限流的,針對客戶端id和ip進行限流。
因為一般用的是ip限流,看下ip限制怎麼使用的,畢竟主要還是拿來用的嘛。
本來還想根據文檔先寫個小demo,然後發現官方已經寫了demo。
直接看demo(只看ip限制部分的)。
先來看下ConfigureServices:
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
services.AddInMemoryRateLimiting();
從上面看,那麼配置ip 限制的有兩個配置,一個配置是IpRateLimitOptions,另外一個配置是IpRateLimitPolicies。
那麼為什麼要設計成兩個配置呢?一個配置不是更香嗎?
官方設計理念是這樣的:
//github.com/stefanprodan/AspNetCoreRateLimit/wiki/IpRateLimitMiddleware#defining-rate-limit-rules
IpRateLimiting Configuration and general rules appsettings.json
IpRateLimitPolicies Override general rules for specific IPs appsettings.json
原來IpRateLimiting 是限制普遍的ip,而IpRateLimitPolicies 是限制一些特殊的ip。
比如說有些api對內又對外的,普遍的ip對外限制是1分鐘300次,如果有個大客戶特殊需求且固定ip的,需要限制是1分鐘是10000次的,那麼就可以這樣特殊處理,而不用另外寫code來維護,成本問題。
故而我們寫中間件組件的時候也可以參考這個來做,特殊的怎麼處理,普遍的怎麼處理,當然也不能盲目的設計。
然後看:AddInMemoryRateLimiting
public static IServiceCollection AddInMemoryRateLimiting(this IServiceCollection services)
{
services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>();
return services;
}
裡面注入了MemoryCacheIpPolicyStore、MemoryCacheClientPolicyStore、MemoryCacheRateLimitCounterStore、AsyncKeyLockProcessingStrategy。
分別看下這幾個東西。
MemoryCacheIpPolicyStore:
public class MemoryCacheIpPolicyStore : MemoryCacheRateLimitStore<IpRateLimitPolicies>, IIpPolicyStore
{
private readonly IpRateLimitOptions _options;
private readonly IpRateLimitPolicies _policies;
public MemoryCacheIpPolicyStore(
IMemoryCache cache,
IOptions<IpRateLimitOptions> options = null,
IOptions<IpRateLimitPolicies> policies = null) : base(cache)
{
_options = options?.Value;
_policies = policies?.Value;
}
public async Task SeedAsync()
{
// on startup, save the IP rules defined in appsettings
if (_options != null && _policies != null)
{
await SetAsync($"{_options.IpPolicyPrefix}", _policies).ConfigureAwait(false);
}
}
}
這個是用例存儲IpRateLimitPolicies(ip限制)。
MemoryCacheIpPolicyStore 這個名字起的有點意思,MemoryCache 是記憶體快取,IpPolicy ip策略,store 存儲。
分別是存儲空間、物品、功能的組合。所以這個庫應該是外國人寫的,一般來說中國人會這樣改:IpPolicyMemoryCacheStore,估計是因為強調故而把MemoryCache放到前面去了。
這裡我剛開始有點不理解,本來已經可以讀取到了options,那麼按照options操作就很方便了。
那麼為啥要用快取到記憶體中呢?後來大體的通讀了一下,是因為_policies(特殊制定的ip規則)很多地方都要使用到,一方面是為了解耦,另外一方面呢,是因為下面這個。
[HttpPost]
public void Post()
{
var pol = _ipPolicyStore.Get(_options.IpPolicyPrefix);
pol.IpRules.Add(new IpRateLimitPolicy
{
Ip = "8.8.4.4",
Rules = new List<RateLimitRule>(new RateLimitRule[] {
new RateLimitRule {
Endpoint = "*:/api/testupdate",
Limit = 100,
Period = "1d" }
})
});
_ipPolicyStore.Set(_options.IpPolicyPrefix, pol);
}
是可以動態設置特殊ip的一些配置的。 那麼裡面也考慮到了分散式的一些行為,比如把快取放到redis這種隔離快取中。
如果將_policies 封裝到memory cache 中,那麼和redis cache形成了一套適配器。個人認為是從設計方面考慮的。
然後看下這個方法,裡面就是以IpRateLimiting的IpPolicyPrefix 作為key,然後存儲了IpRateLimitPolicies。
public async Task SeedAsync()
{
// on startup, save the IP rules defined in appsettings
if (_options != null && _policies != null)
{
await SetAsync($"{_options.IpPolicyPrefix}", _policies).ConfigureAwait(false);
}
}
具體的SetAsync 如下:
public Task SetAsync(string id, T entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default)
{
var options = new MemoryCacheEntryOptions
{
Priority = CacheItemPriority.NeverRemove
};
if (expirationTime.HasValue)
{
options.SetAbsoluteExpiration(expirationTime.Value);
}
_cache.Set(id, entry, options);
return Task.CompletedTask;
}
然後這裡值得注意的是_options.IpPolicyPrefix,這個值如果是分散式那麼應該值得關注一下,因為我們有不同應用服務,如果希望不同的應用服務用到不同的ip限制,那麼IpPolicyPrefix 最好改成應用名,而不是使用默認值。
那麼看下MemoryCacheClientPolicyStore:
public class MemoryCacheClientPolicyStore : MemoryCacheRateLimitStore<ClientRateLimitPolicy>, IClientPolicyStore
{
private readonly ClientRateLimitOptions _options;
private readonly ClientRateLimitPolicies _policies;
public MemoryCacheClientPolicyStore(
IMemoryCache cache,
IOptions<ClientRateLimitOptions> options = null,
IOptions<ClientRateLimitPolicies> policies = null) : base(cache)
{
_options = options?.Value;
_policies = policies?.Value;
}
public async Task SeedAsync()
{
// on startup, save the IP rules defined in appsettings
if (_options != null && _policies?.ClientRules != null)
{
foreach (var rule in _policies.ClientRules)
{
await SetAsync($"{_options.ClientPolicyPrefix}_{rule.ClientId}", new ClientRateLimitPolicy { ClientId = rule.ClientId, Rules = rule.Rules }).ConfigureAwait(false);
}
}
}
}
這個就是client id的限制的快取的,和上面一樣就不看了。
MemoryCacheRateLimitCounterStore:
public class MemoryCacheRateLimitCounterStore : MemoryCacheRateLimitStore<RateLimitCounter?>, IRateLimitCounterStore
{
public MemoryCacheRateLimitCounterStore(IMemoryCache cache) : base(cache)
{
}
}
這裡面沒有啥子。但是從名字上猜測,裡面是快取每個ip請求次數的當然還有時間,主要起快取作用。
最後一個:AsyncKeyLockProcessingStrategy
public class AsyncKeyLockProcessingStrategy : ProcessingStrategy
{
private readonly IRateLimitCounterStore _counterStore;
private readonly IRateLimitConfiguration _config;
public AsyncKeyLockProcessingStrategy(IRateLimitCounterStore counterStore, IRateLimitConfiguration config)
: base(config)
{
_counterStore = counterStore;
_config = config;
}
/// The key-lock used for limiting requests.
private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock();
public override async Task<RateLimitCounter> ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, ICounterKeyBuilder counterKeyBuilder, RateLimitOptions rateLimitOptions, CancellationToken cancellationToken = default)
{
var counter = new RateLimitCounter
{
Timestamp = DateTime.UtcNow,
Count = 1
};
var counterId = BuildCounterKey(requestIdentity, rule, counterKeyBuilder, rateLimitOptions);
// serial reads and writes on same key
using (await AsyncLock.WriterLockAsync(counterId).ConfigureAwait(false))
{
var entry = await _counterStore.GetAsync(counterId, cancellationToken);
if (entry.HasValue)
{
// entry has not expired
if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow)
{
// increment request count
var totalCount = entry.Value.Count + _config.RateIncrementer?.Invoke() ?? 1;
// deep copy
counter = new RateLimitCounter
{
Timestamp = entry.Value.Timestamp,
Count = totalCount
};
}
}
// stores: id (string) - timestamp (datetime) - total_requests (long)
await _counterStore.SetAsync(counterId, counter, rule.PeriodTimespan.Value, cancellationToken);
}
return counter;
}
}
估摸著是執行具體計數邏輯的,那麼等執行中間件的時候在看。
後面有寫入了一個:services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
那麼這個RateLimitConfiguration 是做什麼的呢?
public class RateLimitConfiguration : IRateLimitConfiguration
{
public IList<IClientResolveContributor> ClientResolvers { get; } = new List<IClientResolveContributor>();
public IList<IIpResolveContributor> IpResolvers { get; } = new List<IIpResolveContributor>();
public virtual ICounterKeyBuilder EndpointCounterKeyBuilder { get; } = new PathCounterKeyBuilder();
public virtual Func<double> RateIncrementer { get; } = () => 1;
public RateLimitConfiguration(
IOptions<IpRateLimitOptions> ipOptions,
IOptions<ClientRateLimitOptions> clientOptions)
{
IpRateLimitOptions = ipOptions?.Value;
ClientRateLimitOptions = clientOptions?.Value;
}
protected readonly IpRateLimitOptions IpRateLimitOptions;
protected readonly ClientRateLimitOptions ClientRateLimitOptions;
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());
}
protected string GetClientIdHeader()
{
return ClientRateLimitOptions?.ClientIdHeader ?? IpRateLimitOptions?.ClientIdHeader;
}
protected string GetRealIp()
{
return IpRateLimitOptions?.RealIpHeader ?? ClientRateLimitOptions?.RealIpHeader;
}
}
重點看:
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());
}
這裡只看ip部分:
protected string GetRealIp()
{
return IpRateLimitOptions?.RealIpHeader ?? ClientRateLimitOptions?.RealIpHeader;
}
那麼這個IpHeaderResolveContributor是什麼呢?
public class IpHeaderResolveContributor : IIpResolveContributor
{
private readonly string _headerName;
public IpHeaderResolveContributor(
string headerName)
{
_headerName = headerName;
}
public string ResolveIp(HttpContext httpContext)
{
IPAddress clientIp = null;
if (httpContext.Request.Headers.TryGetValue(_headerName, out var values))
{
clientIp = IpAddressUtil.ParseIp(values.Last());
}
return clientIp?.ToString();
}
}
原來是配置是從header的哪個位置獲取ip。官網demo中給的是”RealIpHeader”: “X-Real-IP”。從header部分的RealIpHeader獲取。
同樣,官方也默認提供了IpResolvers.Add(new IpConnectionResolveContributor());。
public class IpConnectionResolveContributor : IIpResolveContributor
{
public IpConnectionResolveContributor()
{
}
public string ResolveIp(HttpContext httpContext)
{
return httpContext.Connection.RemoteIpAddress?.ToString();
}
}
從httpContext.Connection.RemoteIpAddress 中獲取ip,那麼問題來了,RemoteIpAddress 是如何獲取的呢? 到底X-Real-IP 獲取的ip準不準呢?會在.net core 細節篇中介紹。
回到原始。現在已經注入了服務,那麼如何把中間件注入進去呢?
在Configure 中:
app.UseIpRateLimiting();
將會執行中間件:IpRateLimitMiddleware
public class IpRateLimitMiddleware : RateLimitMiddleware<IpRateLimitProcessor>
{
private readonly ILogger<IpRateLimitMiddleware> _logger;
public IpRateLimitMiddleware(RequestDelegate next,
IProcessingStrategy processingStrategy,
IOptions<IpRateLimitOptions> options,
IRateLimitCounterStore counterStore,
IIpPolicyStore policyStore,
IRateLimitConfiguration config,
ILogger<IpRateLimitMiddleware> logger
)
: base(next, options?.Value, new IpRateLimitProcessor(options?.Value, counterStore, policyStore, config, processingStrategy), config)
{
_logger = logger;
}
protected override void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule)
{
_logger.LogInformation($"Request {identity.HttpVerb}:{identity.Path} from IP {identity.ClientIp} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.Count - rule.Limit}. Blocked by rule {rule.Endpoint}, TraceIdentifier {httpContext.TraceIdentifier}. MonitorMode: {rule.MonitorMode}");
}
}
查看:RateLimitMiddleware
裡面就是具體的invoke中間件程式碼了。
結
因為篇幅有限,後一節invoke逐行分析其如何實現的。
以上只是個人看源碼的過程,希望能得到各位的指點,共同進步。
另外.net core 細節篇整理進度為40%。