作者:Arvin Kahbazi、Maarten Balliauw 和 Rick Anderson
Microsoft.AspNetCore.RateLimiting 中介軟體具有流量限制功能。 應用程式會設定速率限制原則,然後將原則附加至端點。 使用速率限制的應用程式應該先仔細進行載入測試及檢閱,然後再進行部署。 如需詳細資訊,請參閱本文中的測試具有速率限制的端點。
如需速率限制的簡介,請參閱速率限制中介軟體。
為何使用速率限制
速率限制可用來管理應用程式連入要求流程。 實作速率限制的主要原因:
防止濫用:速率限制可藉由限制使用者或用戶端在指定時間內提出的要求數目,協助保護應用程式免於濫用。 這種保護對於公開 API 尤其重要。
確保公平使用:透過設定限制,防止使用者壟斷系統,確保所有使用者都能公平使用資源。
資源保護:速率限制透過控制可處理的請求數量,有助於防止伺服器過載。 它保護後端資源不會被過度淹沒。
提升安全性:它可透過限制請求處理速率,降低拒絕服務(DoS)攻擊的風險。 這讓攻擊者更難淹沒系統。
提升效能:透過控制來訪請求的速率,您可以維持應用程式的最佳效能與回應速度,確保更佳的使用者體驗。
成本管理:對於根據使用量產生成本的服務,速率限制可藉由控制處理的要求量來協助管理和預測費用。
在 ASP.NET Core 應用程式中實作速率限制,有助於維持穩定性、安全性與效能。 結果是為所有使用者提供可靠且高效的服務。
防止 DDoS 攻擊
雖然速率限制可藉由限制處理要求的速率來協助降低阻斷服務 (DoS) 攻擊的風險,但這不是分散式阻斷服務 (DDoS) 攻擊的完整解決方案。 DDoS 攻擊牽涉到多個系統,讓應用程式遭受大量要求,因此難以單獨處理速率限制。
針對強固的 DDoS 保護,請考慮使用商業 DDoS 保護服務。 這些服務提供進階功能,例如:
- 流量分析:持續監控與分析進站流量,以即時偵測並減輕 DDoS 攻擊。
- 延展性:將流量分散到多部伺服器和數據中心,以處理大規模攻擊的能力。
- 自動化緩解:自動化回應機制,能快速阻擋惡意流量,無需人工介入。
- 全球網路:一個全球伺服器網絡,用以吸收並減輕靠近攻擊源頭的攻擊。
- 持續更新:商業服務持續追蹤並更新其防護機制,以適應新且不斷演變的威脅。
使用雲端裝載服務時,DDoS 保護通常是作為裝載解決方案的一部分,例如 Azure Web 應用程式防火牆、AWS Shield 或 Google Cloud Armor。 專用保護可作為 Web 應用程式防火牆 (WAF) 或 CDN 解決方案的一部分,例如 Cloudflare 或 Akamai Kona Site Defender
實作商業 DDoS 保護服務搭配速率限制可以提供全面的防禦策略,確保應用程式的穩定性、安全性和效能。
使用速率限制中介軟體
下列步驟示範如何在 ASP.NET Core 應用程式中使用速率限制中間件:
- 設定速率限制服務。
在 Program.cs 檔案中,新增適當的速率限制原則來設定速率限制服務。 將政策定義為全域或命名政策。 以下範例允許每位使用者(身份識別)或全域每分鐘 10 次請求:
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
}));
});
命名策略必須明確套用到頁面或端點。 以下範例新增了一個固定的視窗限制器策略,該 "fixed" 策略需在後續端點上加入:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 4;
opt.Window = TimeSpan.FromSeconds(12);
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 2;
});
});
var app = builder.Build();
當你透過選項設定全域限制器時,全域限制器會自動套用到所有端點 。GlobalLimiter。
啟用速率限制中間件
在
Program.cs檔案中,藉由呼叫 UseRateLimiter來啟用速率限制中間件:
app.UseRouting();
app.UseRateLimiter();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Run();
將速率限制原則套用至端點或頁面
對 Web API 端點施加速率限制
將具名原則套用至端點或群組,例如:
app.MapGet("/api/resource", () => "This endpoint is rate limited")
.RequireRateLimiting("fixed"); // Apply specific policy to an endpoint
對 MVC 控制器施加速率限制
將設定的速率限制原則套用至特定端點或全域。 例如,若要將「固定」原則套用至所有控制器端點:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers().RequireRateLimiting("fixed");
});
將速率限制套用至伺服器端 Blazor 應用程式
若要設定所有應用程式可路由傳送 Razor 元件的速率限制,請在 RequireRateLimiting 檔案的 MapRazorComponents 呼叫上指定速率限制原則名稱 Program。 在下列範例中,會套用名為 「policy」 的速率限制原則:
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.RequireRateLimiting("policy");
若要為單一可路由的 Razor 元件或透過匯入檔案(_Imports.razor)指定的元件資料夾設定政策,請套用帶有政策名稱的 [EnableRateLimiting] 屬性。 在下列範例中,會套用名為 「override」 的速率限制原則。 原則會取代目前套用至端點的任何原則。 全域限制器仍會在套用此屬性的端點上執行。
@page "/counter"
@using Microsoft.AspNetCore.RateLimiting
@attribute [EnableRateLimiting("override")]
<h1>Counter</h1>
如果未對RequireRateLimiting呼叫MapRazorComponents,則僅透過 imports 檔案將 屬性 套用至可路由元件或元件資料夾。
使用屬性[DisableRateLimiting]來關閉可路由元件或元件資料夾的速率限制,透過匯入檔案。
速率限制器演算法
RateLimiterOptionsExtensions 類別提供下列擴充方法以進行速率限制:
固定、滑動和權杖限制器都會限制一段時間內的要求數目上限。 並行限制器只會限制並行要求的數目,而不會限制一段時間內的要求數目。 選擇限制器時,請考慮端點的成本。 端點的成本包括所使用的資源,如時間、資料存取、CPU 與 I/O。
固定視窗限制器
AddFixedWindowLimiter 方法會使用固定時間窗口來限制要求。 當時間窗口結束時,會重新開始新的時間窗口,請求限制會重置。
請考慮下列程式碼:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: "fixed", options =>
{
options.PermitLimit = 4;
options.Window = TimeSpan.FromSeconds(12);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 2;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
.RequireRateLimiting("fixed");
app.Run();
上述 程式碼:
- 呼叫 AddRateLimiter 以將速率限制服務新增至服務集合。
- 呼叫
AddFixedWindowLimiter,以建立具有原則名稱"fixed"的固定視窗限制器,並設定相關配置: - 將 PermitLimit 設定為 4,並將時間 Window 設定為 12。 每個 12 秒視窗最多允許 4 個要求。
- QueueProcessingOrder 至 OldestFirst。
- QueueLimit 為 2(將此設定為 0 以停用佇列機制)。
- 呼叫 UseRateLimiter 以啟用速率限制。
應用程式應該使用組態來設定限制器選項。 下列程式碼使用 MyRateLimitOptions 作為配置來更新前述程式碼:
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Fixed Window Limiter {GetTicks()}"))
.RequireRateLimiting(fixedPolicy);
app.Run();
在使用速率限制端點特定 API 時,必須在 UseRateLimiter 之後呼叫 UseRouting。 例如,如果使用 [EnableRateLimiting] 屬性,則必須在 UseRateLimiter 之後呼叫 UseRouting。 只呼叫全域限制器時,可以在 UseRateLimiter 之前呼叫 UseRouting。
滑動視窗限制器
滑動視窗演算法:
- 類似於固定視窗限制器,但會為每個視窗新增區段。 視窗每個區段間隔滑動一次。 區段間隔為 (視窗時間)/(每個視窗的區段)。
- 將窗口可接受的要求數量限制為
permitLimit之內。 - 每個時間視窗都被分成
n個區段。 - 取自前一個窗口的已過期時間區段(即目前區段的前
n區段)的要求會被新增至目前的區段。 我們將過期最久時間區段的前一個視窗稱為過期的區段。
請考慮下表,其中顯示滑動視窗限制器,包含 30 秒的視窗、每個視窗三個區段,以及 100 個要求的限制:
- 第一列和第一欄顯示時間段。
- 第二列會顯示剩餘可用的請求。 系統會計算其餘要求,以可用的要求減去已處理的要求加上回收的要求。
- 每一個請求都會沿著藍色對角線移動。
- 從時間 30 開始,來自過期的時間段的請求會被新增回請求限制,如紅線所示。
下表以不同的格式顯示上一個圖表中的資料。
| 時間 | 可用 | 被拿走 | 回收自過期的產品 | 繼續 |
|---|---|---|---|---|
| 0 | 100 | 20 | 0 | 80 |
| 10 | 80 | 30 | 0 | 50 |
| 20 | 50 | 40 | 0 | 10 |
| 30 | 10 | 30 | 20 | 0 |
| 40 | 0 | 10 | 30 | 20 |
| 50 | 20 | 10 | 40 | 50 |
| 六十 | 50 | 35 | 30 | 45 |
下列程式碼使用滑動視窗速率限制器:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Sliding Window Limiter {GetTicks()}"))
.RequireRateLimiting(slidingPolicy);
app.Run();
令牌桶限制器
令牌桶限制器類似於滑動視窗限制器,但不是將從過期區段取出的請求添加回去,而是在每個補充期間新增固定數量的令牌。 每個部分新增的令牌無法使可用令牌增加到超過令牌桶限制的數量。 下表顯示權杖貯體限制器,其限制為 100 個權杖和 10 秒的補充期間。
| 時間 | 可用 | 被拿走 | 已新增 | 繼續 |
|---|---|---|---|---|
| 0 | 100 | 20 | 0 | 80 |
| 10 | 80 | 10 | 20 | 90 |
| 20 | 90 | 5 | 15 | 100 |
| 30 | 100 | 30 | 20 | 90 |
| 40 | 90 | 6 | 16 | 100 |
| 50 | 100 | 40 | 20 | 80 |
| 六十 | 80 | 50 | 20 | 50 |
以下程式碼會使用令牌桶限制器:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var tokenPolicy = "token";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddTokenBucketLimiter(policyName: tokenPolicy, options =>
{
options.TokenLimit = myOptions.TokenLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
options.ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod);
options.TokensPerPeriod = myOptions.TokensPerPeriod;
options.AutoReplenishment = myOptions.AutoReplenishment;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}"))
.RequireRateLimiting(tokenPolicy);
app.Run();
當 AutoReplenishment 設定為 true 時,內部計時器會每 ReplenishmentPeriod 補充權杖;當設定為 false 時,應用程式必須在限制器上呼叫 TryReplenish。
並行限制器
並行限制器會限制並行要求的數目。 每個要求都會將併發限制減少一個。 當要求完成時,限制就會增加一個。 不同於其他限制指定期間要求總數的要求限制器,並行限制器只會限制並行要求的數目,且不會限制一段時間內的要求數目。
下列程式碼使用並行限制器:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var concurrencyPolicy = "Concurrency";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddConcurrencyLimiter(policyName: concurrencyPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", async () =>
{
await Task.Delay(500);
return Results.Ok($"Concurrency Limiter {GetTicks()}");
}).RequireRateLimiting(concurrencyPolicy);
app.Run();
速率限制分區
速率限制分區將流量分割成獨立的桶,每個桶都有自己的速率限制計數器。 此方法提供比單一全域計數器更細緻的控制。 不同的金鑰,如使用者 ID、IP 位址或 API 金鑰,定義分割桶。
分區的優點
- 公平性:一個用戶無法消耗所有人的全部費率上限。
- 粒度:針對不同的使用者和資源設定不同的限制。
- 安全性:更有效地防止針對性濫用。
- 分級服務:支援不同限制的服務層級。
分割速率限制可讓您更精細地控制如何管理 API 流量,同時確保公平的資源配置。
依 IP 位址分類
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 50,
Window = TimeSpan.FromMinutes(1)
}));
依使用者身份分類
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? "anonymous",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
依據 API 金鑰
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
string apiKey = httpContext.Request.Headers["X-API-Key"].ToString() ?? "no-key";
// Different limits based on key tier
return apiKey switch
{
"premium-key" => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: apiKey,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 1000,
Window = TimeSpan.FromMinutes(1)
}),
_ => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: apiKey,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}),
};
});
依端點路徑
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
string path = httpContext.Request.Path.ToString();
// Different limits for different paths
if (path.StartsWith("/api/public"))
{
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: $"{httpContext.Connection.RemoteIpAddress}-public",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 30,
Window = TimeSpan.FromSeconds(10)
});
}
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
});
});
建立鏈結限制器
API CreateChained 接受多個 PartitionedRateLimiter 實例並將它們合併成一個 PartitionedRateLimiter。 合併的限制器會依序執行所有輸入限制器。 由於結果被 PartitionedRateLimiter 指派到 GlobalLimiter,鏈條適用於每個端點。 若要改為對特定端點串接限制器,請使用命名策略,如 在命名策略中串接限制器 所示。
下列程式碼會使用 CreateChained:
using System.Globalization;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(_ =>
{
_.OnRejected = async (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", cancellationToken);
};
_.GlobalLimiter = PartitionedRateLimiter.CreateChained(
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 4,
Window = TimeSpan.FromSeconds(2)
});
}),
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 20,
Window = TimeSpan.FromSeconds(30)
});
}));
});
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"));
app.Run();
欲了解更多資訊,請參閱 CreateChained 原始碼。
命名政策中的鏈限制器
CreateChained 連鎖全 域 限制器,適用於每個端點。 若要結合多種限制器類型並將其套用至特定端點,請在具名原則中使用 CreateChained 串接限制器。 此多載會傳回單一的 RateLimiter,該 RateLimiter 會依序執行各個限制器,而這正是具名原則的分割區處理站函式所需的傳回型別。
下列 "combined" 策略會將權杖桶限制器與並行限制器以 RateLimiter.CreateChained 串接,接著使用 RequireRateLimiting 將該策略套用至單一端點:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("combined", httpContext =>
{
// Partition on the authenticated identity name when available. Each distinct key creates and
// caches its own limiter, so partitioning on unbounded user-controlled
// input can exhaust memory (a DoS risk).
string partitionKey = httpContext.User.Identity?.Name ?? "anonymous";
return RateLimitPartition.Get(partitionKey, _ =>
RateLimiter.CreateChained(
new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
TokenLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 5,
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
TokensPerPeriod = 10,
AutoReplenishment = true
}),
new ConcurrencyLimiter(new ConcurrencyLimiterOptions
{
PermitLimit = 5,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2
})));
});
});
var app = builder.Build();
app.MapGet("/api/resource", () => "This endpoint uses multiple limiters")
.RequireRateLimiting("combined");
請求必須向鏈結中的每個限制器取得租約才能繼續,而限制器會依傳遞給 RateLimiter.CreateChained 的順序執行。 若某個限制器拒絕該請求,則該請求會被拒絕,而先前從鏈中較前面的限制器取得的租約會依相反順序釋放。
在命名政策中串接限制器時,請注意以下幾點:
- 釋放租約會歸還並行限制器的許可。 基於時間的限制器(權杖桶、固定視窗和滑動視窗)在鏈中較後面的限制器拒絕請求時,不會歸還先前已取得的許可,因此在安排鏈中各限制器的順序時,請將這點納入考量。
- 直接建立一個將
AutoReplenishment設為true的TokenBucketRateLimiter,會使每個限制器執行個體都有自己的計時器。AddTokenBucketLimiter和RateLimitPartition.GetTokenBucketLimiter輔助函式則會將AutoReplenishment設為false,並從單一共用計時器為其所有限制器補充配額。 -
RateLimiter.CreateChained不會釋放傳遞給它的限制器。 在前述範例中,分割區快取鏈式限制器,框架管理其壽命。 如果你在分區外建立連鎖限制器,當內部限制器不再使用時就丟棄它們。 - 當鏈條應該適用於每個端點時,偏好全域
PartitionedRateLimiter.CreateChained方法。 只有在必須將鏈限定於特定端點時,才搭配RateLimiter.CreateChained使用具名原則。
確定在請求被速率限制時的處理方式
針對簡單案例,您可以只設定狀態代碼:
builder.Services.AddRateLimiter(options =>
{
// Set a custom status code for rejections
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// Rate limiter configuration...
});
最常見的做法是在設定速率限制時註冊 OnRejected 回調:
builder.Services.AddRateLimiter(options =>
{
// Rate limiter configuration...
options.OnRejected = async (context, cancellationToken) =>
{
// Custom rejection handling logic
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.Headers["Retry-After"] = "60";
await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Please try again later.", cancellationToken);
// Optional logging
logger.LogWarning("Rate limit exceeded for IP: {IpAddress}",
context.HttpContext.Connection.RemoteIpAddress);
};
});
另一個選項是將要求排入佇列:
請求排隊
當你啟用佇列時,如果請求超過速率限制,系統會將其放入佇列。 請求會在佇列中等待,直到有可用的許可證,或發生逾時。 系統會依照可設定的佇列順序處理請求。
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", options =>
{
options.PermitLimit = 10; // Allow 10 requests
options.Window = TimeSpan.FromSeconds(10); // Per 10-second window
options.QueueLimit = 5; // Queue up to 5 additional requests
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; // Process oldest requests first
options.AutoReplenishment = true; // Default: automatically replenish permits
});
});
EnableRateLimiting 與 DisableRateLimiting 屬性
將 [DisableRateLimiting] 和 [EnableRateLimiting] 屬性套用至控制器、動作方法或 Razor 頁面。 對於 Razor Pages,請將該屬性套用至 Razor 頁面,而不是頁面處理常式。 例如,你不能套用 [EnableRateLimiting] 到 OnGet、 或其他 OnPost頁面處理程式。
該 [DisableRateLimiting] 屬性 會禁用 控制器、動作方法或 Razor 頁面的速率限制,無論是否套用了命名的速率限制器或全域限制器。 例如,請考慮下列程式碼,其會呼叫 RequireRateLimiting,將 fixedPolicy 速率限制套用至所有控制器端點:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages().RequireRateLimiting(slidingPolicy);
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy);
app.Run();
在下列程式碼中,[DisableRateLimiting] 會停用速率限制,並覆寫套用至 [EnableRateLimiting("fixed")] 的 Home2Controller,並在 app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) 中呼叫 Program.cs:
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}
public ActionResult Index()
{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
在上述程式碼中,[EnableRateLimiting("sliding")]不會套用至 Privacy 動作方法,因為 Program.cs 稱為 app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)。
請考慮下列未在 RequireRateLimiting 或 MapRazorPages 上呼叫 MapDefaultControllerRoute 的程式碼:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages();
app.MapDefaultControllerRoute(); // RequireRateLimiting not called
app.Run();
請考慮下列控制器:
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}
public ActionResult Index()
{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
在上述控制器中:
-
"fixed"原則速率限制器會套用至沒有EnableRateLimiting和DisableRateLimiting屬性的所有動作方法。 -
"sliding"原則速率限制器會套用至Privacy動作。 - 已停用
NoLimit動作方法上的速率限制。
速率限制指標
速率限制中介軟體提供 內建指標與監控 功能,幫助您了解速率限制如何影響應用程式效能與使用者體驗。 有關指標列表,請參見 Microsoft.AspNetCore.RateLimiting。
使用限制速率測試端點
在部署使用速率限制的應用程式到生產環境前,先對應用程式進行壓力測試,以驗證你使用的速率限制和選項。 例如,使用像 BlazeMeter 或 Apache JMeter HTTP(S) Test Script Recorder 這類工具建立 JMeter 腳本,然後載入 Azure 負載測試。
如果你透過使用者輸入建立分割區,你的應用程式就會容易受到 阻斷服務 (DoS)攻擊。 例如,如果你使用用戶端 IP 位址來建立分割區,你的應用程式就會容易受到利用 IP 來源位址偽造的阻斷服務攻擊。 如需詳細資訊,請參閱 BCP 38 RFC 2827 網路輸入篩選:拒絕採用 IP 來源位址詐騙的服務攻擊。
其他資源
- Maarten Balliauw 的速率限制中介軟體提供絕佳的速率限制簡介和概觀。
- 在 .NET 中限制 HTTP 處理程序的速度