一、環境
1.vs2019
2..Net Core 3.1
3.引用 AspNetCoreRateLimit 4.0.1
二、基礎使用
1.設定
在Startup檔案中配置如下,把配置項都放在前面:
public void ConfigureServices(IServiceCollection services) { // 從appsettings.json中載入ip限流配置通用規則 services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting")); // 從appsettings.json中載入ip限流規則 services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimiting:IpRateLimitPolicies")); // 從appsettings.json中載入客戶端限流配置通用規則 services.Configure<ClientRateLimitOptions>(Configuration.GetSection("IpRateLimiting")); // 從appsettings.json中載入客戶端限流規則 services.Configure<ClientRateLimitPolicies>(Configuration.GetSection("IpRateLimiting:ClientRateLimitPolicies")); // 注入計數器和規則儲存 services.AddInMemoryRateLimiting(); // 配置(解析器、計數器金鑰生成器) services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>(); //解析clientid和ip的使用有用,如果預設沒有啟用,則此處啟用 //services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //呼叫ip限流方式和客戶端限流方式 //只能選用一個,後一個呼叫的生效,也就是說ip規則限流和客戶端限流的特殊規則不能同時使用,但是通用規則不影響 app.UseIpRateLimiting(); app.UseClientRateLimiting(); }
2.規則設定
規則的設定分為兩個大類:通過IP限流和通過客戶端限流。都通過配置檔案來配置引數,在appsettings.json中配置如下(也可以另起配置檔案):
"IpRateLimiting": { "EnableEndpointRateLimiting": false, "StackBlockedRequests": false, "RealIpHeader": "X-Real-IP", "ClientIdHeader": "X-ClientId", "HttpStatusCode": 429, //"IpWhitelist": [ "198.0.0.1", "::1/10", "192.168.0.13/24" ], "EndpointWhitelist": [ "get:/api/license", "*:/api/status" ], "ClientWhitelist": [ "dev-id-1", "dev-id-2" ], "QuotaExceededResponse": { "Content": "{{\"code\":429,\"msg\":\"Visit too frequently, please try again later\",\"data\":null}}", "ContentType": "application/json;utf-8", "StatusCode": 429 }, "GeneralRules": [ { "Endpoint": "*", "Period": "1s", "Limit": 2 } ], "ClientRateLimitPolicies": { "ClientRules": [ { "ClientId": "client-id-1", "Rules": [ { "Endpoint": "*", "Period": "1s", "Limit": 10 }, { "Endpoint": "*", "Period": "15m", "Limit": 200 } ] } ] }, "IpRateLimitPolicies": { "IpRules": [ { "Ip": "84.247.85.224", "Rules": [ { "Endpoint": "*", "Period": "1s", "Limit": 10 }, { "Endpoint": "*", "Period": "15m", "Limit": 200 } ] } ] } }
各配置項的說明如下:
EnableEndpointRateLimiting:設定為true,則端點規則為 * 的時候所有的謂詞如GET、POST等分別享有限制次數。例如,如果您為*:/api/values
客戶端設定每秒GET /api/values
5 次呼叫的限制,則每秒可以呼叫5 次,但也可以呼叫5 次PUT /api/values。
如果設定為false,則上述例子中GET、POST等請求共享次數限制。是否共享限制次數的設定。這裡有個注意的地方,就是當該引數設定為false的時候,只有端點設定為星號*的規則有效,其他規則無效,設定為true時所有規則有效。
StackBlockedRequests:設為false的情況下,被拒絕的請求不會加入到計數器中,如一秒內有三個請求,限流規則分別為一秒一次和一分鐘三次,則被拒絕的兩個請求是不會記錄在一分鐘三次的規則中的,也就是說這一分鐘還能呼叫兩次該介面。設定為true的話,則被拒絕的請求也會加入計數器,像上述例子中的情況,一分鐘內就不能呼叫了,三次全部記錄了。
RealIpHeader:與配置項IP白名單IpWhitelist組合使用,如果該引數定義的請求頭名稱存在於一個請求中,並且該引數內容為IP白名單中的IP,則不受限流規則限制。
ClientIdHeader:與配置項客戶端白名單ClientIdHeader組合使用,如果該引數定義的請求頭名稱存在於一個請求中,並且該引數內容為客戶端白名單中的名稱,則不受限流規則限制。
HttpStatusCode:http請求限流後的返回碼。
IpWhitelist:IP白名單,欄位支援支援Ip v4和v6如 "198.0.0.1", "::1/10", "192.168.0.13/24"等。可以配合RealIpHeader引數使用,也單獨使用,請求的ip符合該白名單規則任意一條,則不受限流規則限制。
EndpointWhitelist:終端白名單,符合該終端規則的請求都將不受限流規則影響,如"get:/api/values"表示GET請求的api/values介面不受影響,*表示所有型別的請求。
ClientWhitelist:客戶端白名單,配合ClientIdHeader引數使用,配置客戶端的名稱。
QuotaExceededResponse:限流後的返回值設定,返回內容、狀態碼等。
GeneralRules:通用規則設定,有三個引數為Endpoint、Period和Limit。
Endpoint端點格式為{HTTP_Verb}:{PATH},可以使用星號來定位任何 HTTP 動詞,如get:/api/values。
Period期間格式為{INT}{PERIOD_TYPE},可以使用以下期間型別之一:s、m、h、d,分別為秒分時天。
Limit限制格式為{LONG},訪問次數。
ClientRateLimitPolicies:客戶端限流的特殊配置,規則和通用規則一樣設定,只不過需要配合ClientIdHeader在請求頭中來使用,需要使用app.UseClientRateLimiting();啟用,否則無效。這個引數名稱是可以更改的噢。通用規則和特殊規則是同優先順序的。
IpRateLimitPolicies:IP限流的特殊配置,規則和通用規則一樣設定,只不過需要配合RealIpHeader在請求頭中來使用,需要使用app.UseIpRateLimiting();啟用,否則無效。這個引數名稱是可以更改的噢。通用規則和特殊規則是同優先順序的。
3.特殊規則的啟用
IP和客戶端特殊規則的啟用需要改造Program檔案中的程式入口如下,分別傳送各自的特殊規則:
public static async Task Main(string[] args) { IWebHost webHost = CreateWebHostBuilder(args).Build(); using (var scope = webHost.Services.CreateScope()) { var clientPolicyStore = scope.ServiceProvider.GetRequiredService<IClientPolicyStore>(); await clientPolicyStore.SeedAsync(); var ipPolicyStore = scope.ServiceProvider.GetRequiredService<IIpPolicyStore>(); await ipPolicyStore.SeedAsync(); } await webHost.RunAsync(); }
在ConfigureServices中讀取配置引數,之後是在Startup檔案中的Configure方法選擇app.UseIpRateLimiting()或app.UseClientRateLimiting()啟動IP特殊規則或者客戶端特殊規則,都存在的情況下,先執行的先生效。
三、請求返回頭
限流啟動後,執行限流規則的返回頭會有三個引數分別為:
X-Rate-Limit-Limit:現在時間,如1d。
X-Rate-Limit-Remaining:剩餘可請求次數。
X-Rate-Limit-Reset:下次請求次數重置時間。
多個限制規則會採用最長的週期的規則顯示。
在配置檔案中配置返回資訊,除了返回提示資訊外,還可以返回限制規則提醒,如下
"Content": "{{\"code\":429,\"msg\":\"訪問太頻繁了,每{1}{0}次,請在{2}秒後重試\",\"data\":null}}",
{0}可以替換當前阻止規則規定的次數,{1}可以替換時間區間帶單位s、h等,{2}替換幾秒後嘗試當單位為天或者小時等都會換算成秒。
四、使用Redis儲存
限流規則等目前都是通過記憶體儲存的,我們結合實際會使用redis儲存。使用Microsoft.Extensions.Caching.Redis可以達到這麼目的。
但是好像會存在效能問題,所以我們自己替換,使用的是用CSRedis封裝的方法,不過這裡不做闡述。
我們快取三類資料1、訪問計數2、ip特殊規則3、客戶端特殊規則
1、訪問計數
public class RedisRateLimitCounterStore : IRateLimitCounterStore { private readonly ILogger _logger; private readonly IRateLimitCounterStore _memoryCacheStore; private readonly RedisCache _redisCache; public RedisRateLimitCounterStore( IMemoryCache memoryCache, ILogger<RedisRateLimitCounterStore> logger) { _logger = logger; _memoryCacheStore = new MemoryCacheRateLimitCounterStore(memoryCache); _redisCache = new RedisCache(); } public async Task<bool> ExistsAsync(string id, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); return await TryRedisCommandAsync( () => { return _redisCache.KeyExistsAsync(id, 0); }, () => { return _memoryCacheStore.ExistsAsync(id, cancellationToken); }); } public async Task<RateLimitCounter?> GetAsync(string id, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); return await TryRedisCommandAsync( async () => { var value = await _redisCache.GetStringAsync(id, 0); if (!string.IsNullOrEmpty(value)) { return JsonConvert.DeserializeObject<RateLimitCounter?>(value); } return null; }, () => { return _memoryCacheStore.GetAsync(id, cancellationToken); }); } public async Task RemoveAsync(string id, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); _ = await TryRedisCommandAsync( async () => { await _redisCache.KeyDeleteAsync(id, 0); return true; }, async () => { await _memoryCacheStore.RemoveAsync(id, cancellationToken); return true; }); } public async Task SetAsync(string id, RateLimitCounter? entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); _ = await TryRedisCommandAsync( async () => { var exprie = expirationTime.HasValue ? Convert.ToInt32(expirationTime.Value.TotalSeconds) : -1; await _redisCache.SetStringAsync(id, JsonConvert.SerializeObject(entry.Value), exprie); return true; }, async () => { await _memoryCacheStore.SetAsync(id, entry, expirationTime, cancellationToken); return true; }); } private async Task<T> TryRedisCommandAsync<T>(Func<Task<T>> command, Func<Task<T>> fallbackCommand) { if (_redisCache != null) { try { return await command(); } catch (Exception ex) { _logger.LogError($"Redis command failed: {ex}"); } } return await fallbackCommand(); } }
2、ip特殊規則
public class RedisIpPolicyStore : IIpPolicyStore { private readonly IpRateLimitOptions _options; private readonly IpRateLimitPolicies _policies; private readonly RedisCache _redisCache; public RedisIpPolicyStore( IOptions<IpRateLimitOptions> options = null, IOptions<IpRateLimitPolicies> policies = null) { _options = options?.Value; _policies = policies?.Value; _redisCache = new RedisCache(); } public async Task<bool> ExistsAsync(string id, CancellationToken cancellationToken = default) { return await _redisCache.KeyExistsAsync($"{_options.IpPolicyPrefix}", 0); } public async Task<IpRateLimitPolicies> GetAsync(string id, CancellationToken cancellationToken = default) { string stored = await _redisCache.GetStringAsync($"{_options.IpPolicyPrefix}", 0); if (!string.IsNullOrEmpty(stored)) { return JsonConvert.DeserializeObject<IpRateLimitPolicies>(stored); } return default; } public async Task RemoveAsync(string id, CancellationToken cancellationToken = default) { await _redisCache.DelStringAsync($"{_options.IpPolicyPrefix}", 0); } public async Task SeedAsync() { // on startup, save the IP rules defined in appsettings if (_options != null && _policies != null) { await _redisCache.SetStringAsync($"{_options.IpPolicyPrefix}", JsonConvert.SerializeObject(_policies), 0).ConfigureAwait(false); } } public async Task SetAsync(string id, IpRateLimitPolicies entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default) { var exprie = expirationTime.HasValue ? Convert.ToInt32(expirationTime.Value.TotalSeconds) : -1; await _redisCache.SetStringAsync($"{_options.IpPolicyPrefix}", JsonConvert.SerializeObject(_policies), 0, exprie); } }
3、客戶端特殊規則
public class RedisClientPolicyStore : IClientPolicyStore { private readonly ClientRateLimitOptions _options; private readonly ClientRateLimitPolicies _policies; private readonly RedisCache _redisCache; public RedisClientPolicyStore( IOptions<ClientRateLimitOptions> options = null, IOptions<ClientRateLimitPolicies> policies = null) { _options = options?.Value; _policies = policies?.Value; _redisCache = new RedisCache(); } public async Task<bool> ExistsAsync(string id, CancellationToken cancellationToken = default) { return await _redisCache.KeyExistsAsync($"{_options.ClientPolicyPrefix}", 0); } public async Task<ClientRateLimitPolicy> GetAsync(string id, CancellationToken cancellationToken = default) { string stored = await _redisCache.GetStringAsync($"{_options.ClientPolicyPrefix}", 0); if (!string.IsNullOrEmpty(stored)) { return JsonConvert.DeserializeObject<ClientRateLimitPolicy>(stored); } return default; } public async Task RemoveAsync(string id, CancellationToken cancellationToken = default) { await _redisCache.DelStringAsync($"{_options.ClientPolicyPrefix}", 0); } public async Task SeedAsync() { // on startup, save the IP rules defined in appsettings if (_options != null && _policies != null) { await _redisCache.SetStringAsync($"{_options.ClientPolicyPrefix}", JsonConvert.SerializeObject(_policies), 0).ConfigureAwait(false); } } public async Task SetAsync(string id, ClientRateLimitPolicy entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default) { var exprie = expirationTime.HasValue ? Convert.ToInt32(expirationTime.Value.TotalSeconds) : -1; await _redisCache.SetStringAsync($"{_options.ClientPolicyPrefix}", JsonConvert.SerializeObject(_policies), 0, exprie); } }
之後在Startup檔案中增加對應的注入
services.AddSingleton<IRateLimitCounterStore, RedisRateLimitCounterStore>(); services.AddSingleton<IIpPolicyStore, RedisIpPolicyStore>(); services.AddSingleton<IClientPolicyStore, RedisClientPolicyStore>();
之後執行就可以在redis中看到啦
五、修改規則
規則只能修改IP和客戶端的特殊規則,因為上一部分已經注入了改規則的對應redis增刪查改的功能,所以我們可以利用這些方法重寫規則,如下:
public class ClientRateLimitController : Controller { private readonly ClientRateLimitOptions _options; private readonly IClientPolicyStore _clientPolicyStore; public ClientRateLimitController(IOptions<ClientRateLimitOptions> optionsAccessor, IClientPolicyStore clientPolicyStore) { _options = optionsAccessor.Value; _clientPolicyStore = clientPolicyStore; } [HttpGet] public ClientRateLimitPolicy Get() { return _clientPolicyStore.Get($"{_options.ClientPolicyPrefix}_cl-key-1"); } [HttpPost] public void Post() { var id = $"{_options.ClientPolicyPrefix}_cl-key-1"; var clPolicy = _clientPolicyStore.Get(id); clPolicy.Rules.Add(new RateLimitRule { Endpoint = "*/api/testpolicyupdate", Period = "1h", Limit = 100 }); _clientPolicyStore.Set(id, clPolicy); } }