ASP.NET Core 中的速率限制中介軟體

作者: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 應用程式中使用速率限制中間件:

  1. 設定速率限制服務。

在 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。

  1. 啟用速率限制中間件

    在 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 開始,來自過期的時間段的請求會被新增回請求限制,如紅線所示。

顯示要求、限制和回收位置的表格

下表以不同的格式顯示上一個圖表中的資料。 可用 欄會顯示上一個區段中可用的請求(此為上一個資料列的「結轉」)。 第一行顯示 100 個可用的請求,因為沒有之前的區段。

時間 可用 被拿走 回收自過期的產品 繼續
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 來源位址詐騙的服務攻擊。

其他資源