最近一直在忙(2月份沉迷steam,3月開始工作各種忙),好久沒更新部落格了,不過也積累了一些,忙裡偷閒記錄一下。
這個需求是這樣的,我之前做了個工單系統,現在要對登入、註冊、發起工單這些功能做限流,不能讓使用者請求太頻繁。
從 .Net7 開始,已經有內建的限流功能了,但目前我們的專案還在使用 .Net6 LTS 版本,下一個 LTS 沒釋出之前,暫時不考慮使用 .Net7 這種非 LTS 版本。
然後我找到了這個 AspNetCoreRateLimit 元件,在 Github 上有接近三千個星星,看了一下檔案使用也簡單靈活,於是決定嘗試一下~
專案主頁: https://github.com/stefanprodan/AspNetCoreRateLimit
這是官方的介紹:
AspNetCoreRateLimit is an ASP.NET Core rate limiting solution designed to control the rate of requests that clients can make to a Web API or MVC app based on IP address or client ID.
The AspNetCoreRateLimit NuGet package contains an IpRateLimitMiddleware and a ClientRateLimitMiddleware, with each middleware you can set multiple limits for different scenarios like allowing an IP or Client to make a maximum number of calls in a time interval like per second, 15 minutes, etc. You can define these limits to address all requests made to an API or you can scope the limits to each API URL or HTTP verb and path.
用最近很厲害的 ChatGPT 翻譯一下:
AspNetCoreRateLimit是一個ASP.NET Core速率限制解決方案,旨在基於IP地址或使用者端ID控制使用者端對Web API或MVC應用程式發出請求的速率。
AspNetCoreRateLimit NuGet包 包含一個IpRateLimitMiddleware和一個ClientRateLimitMiddleware,每個中介軟體都可以為不同的場景設定多個限制,比如允許IP或使用者端在時間間隔內進行最大數量的呼叫,比如每秒、15分鐘等。您可以定義這些限制以處理對API發出的所有請求,也可以將限制範圍限定為每個API URL或HTTP動詞和路徑。
這個元件使用起來挺靈活的,直接在 AspNetCore設定 裡定義規則,意味著可以不重新編譯程式就修改限流規則,官方給的例子是直接在 appsettings.json
裡設定,但使用其他設定源理論上也沒問題(設定中心用起來)。
簡單介紹下這個元件的思路
首先它有兩種模式:
IP地址很容易理解,ClientID 我一開始以為是使用者ID,不過看了說明,是一個放在請求頭裡的引數,比如 X-ClientId
,這個要自己實現,可以直接用使用者ID。
為了方便使用,我這個專案裡面直接用IP地址模式。
RateLimit 元件可以設定全域性的限流,也可以設定對某個IP地址(段)進行限流。
為了從 appsettings.json
讀取資料,先在 Program.cs
註冊設定服務
builder.Services.AddOptions();
然後寫個擴充套件方法來註冊 RateLimit 的相關服務
引入名稱空間
using AspNetCoreRateLimit;
using AspNetCoreRateLimit.Redis;
using StackExchange.Redis;
寫個靜態類
public static class ConfigureRateLimit {
public static void AddRateLimit(this IServiceCollection services, IConfiguration conf) {
//load general configuration from appsettings.json
services.Configure<IpRateLimitOptions>(conf.GetSection("IpRateLimiting"));
var redisOptions = ConfigurationOptions.Parse(conf.GetConnectionString("Redis"));
services.AddSingleton<IConnectionMultiplexer>(provider => ConnectionMultiplexer.Connect(redisOptions));
services.AddRedisRateLimiting();
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
}
public static IApplicationBuilder UseRateLimit(this IApplicationBuilder app) {
app.UseIpRateLimiting();
return app;
}
}
來解析一下設定的程式碼。
我暫時不需要對不同的IP地址段應用不同的限流規則
所以直接用 IpRateLimitOptions
services.Configure<IpRateLimitOptions>(conf.GetSection("IpRateLimiting"));
要做根據IP限流,就得記錄每個IP存取了多少次,RateLimit 元件支援多種儲存方式,最簡單的可以直接存記憶體裡,不過為了穩定我還是選擇 Redis。
這幾行程式碼就是設定 Redis 的。
var redisOptions = ConfigurationOptions.Parse(conf.GetConnectionString("Redis"));
services.AddSingleton<IConnectionMultiplexer>(provider => ConnectionMultiplexer.Connect(redisOptions));
services.AddRedisRateLimiting();
最後注入一下 IRateLimitConfiguration
,我猜應該是中介軟體要用到的。至少我目前在 Controller 程式碼裡不需要用到任何跟 RateLimit 有關的程式碼。
寫完了擴充套件方法,回到 Program.cs
builder.Services.AddRateLimit(builder.Configuration);
var app = builder.Build();
app.UseExceptionless();
app.UseStaticFiles(new StaticFileOptions {
ServeUnknownFileTypes = true
});
app.UseRateLimit();
// ...
app.Run();
我這裡把 UseRateLimit
放在 UseStaticFiles
後面,不然頁面裡的靜態檔案都被算進去存取次數,很快就被限流了。
在 appsettings.json
裡寫具體的限流規則。
官網提供的設定規則不能照抄,要理解一下他的檔案
EnableEndpointRateLimiting
- 這個選項要設定為 true ,不然設定的限流是全域性的,不能根據某個路徑單獨設定限流StackBlockedRequests
- 按照預設的設定為 false 就行,設定成 true 的話,一個介面被限流之後再重複請求還會計算到存取次數裡面,這樣有可能導致限流到天荒地老。其他的設定顧名思義,懂的都懂。
GeneralRules
是對具體路徑的限流規則
如果全域性限流,把 EnableEndpointRateLimiting
設定為 false 的話,那就這樣設定,1分鐘只能存取5次
{
"Endpoint": "*",
"Period": "1m",
"Limit": 5
}
Endpoint
可以設定 HTTP方法:路徑
的形式,比如 post:/account/login
具體看檔案吧(參考檔案第三條)
附上我的組態檔,對新增工單、登入、註冊介面進行限流。
{
"IpRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"IpWhitelist": [],
"EndpointWhitelist": [
"get:/api/license",
"*:/api/status"
],
"ClientWhitelist": [
"dev-id-1",
"dev-id-2"
],
"GeneralRules": [
{
"Endpoint": "*:/ticket/add",
"Period": "1m",
"Limit": 5
},
{
"Endpoint": "post:/account/login",
"Period": "1m",
"Limit": 5
},
{
"Endpoint": "post:/account/SignUp",
"Period": "1m",
"Limit": 5
}
],
"QuotaExceededResponse": {
"Content": "{{ \"message\": \"先別急,你存取得太快了!\", \"details\": \"已經觸發限流。限流規則: 每 {1} 只能存取 {0} 次。請 {2} 秒後再重試。\" }}",
"ContentType": "application/json",
"StatusCode": 429
}
}
}
同時自定義了被限流時的提示。
效果如下
{
"message": "先別急,你存取得太快了!",
"details": "已經觸發限流。限流規則: 每 1m 只能存取 5 次。請 16 秒後再重試。"
}