重新整理 .net core 周邊閱讀篇————AspNetCoreRateLimit[一]

前言

整理了一下.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%。