ASP.NET Core のレート制限ミドルウェア

作成者: Arvin Kahbazi、Maarten Balliauw、Rick Anderson

Microsoft.AspNetCore.RateLimiting ミドルウェアは、レート制限ミドルウェアです。 アプリでレート制限ポリシーを構成し、エンドポイントにポリシーをアタッチします。 レート制限を使用するアプリは、デプロイする前に慎重にロード テストし、確認する必要があります。 詳細については、この記事の「レート制限を使用したエンドポイントのテスト」を参照してください。

レート制限の概要については、「レート制限ミドルウェア」を参照してください。

レート制限を使用する理由

レート制限は、アプリへの受信要求のフローを管理するために使用できます。 レート制限を実装する主な理由:

  • 不正使用の防止: レート制限は、ユーザーまたはクライアントが特定の期間内に行うことができる要求の数を制限することで、アプリを悪用から保護するのに役立ちます。 この保護は、パブリック API にとって特に重要です。
  • 公平な使用の確保: ユーザーがシステムを独占できないように制限を設定することで、すべてのユーザーがリソースに公平にアクセスできるようにします。
  • リソースの保護: レート制限は、処理できる要求の数を制御することで、サーバーの過負荷を防ぐのに役立ちます。 バックエンド リソースが過剰に使用されないように保護します。
  • セキュリティの強化: 要求が処理される速度を制限することで、サービス拒否 (DoS) 攻撃のリスクを軽減できます。 これにより、攻撃者がシステムをあふれさせるのが困難になります。
  • パフォーマンスの向上: 受信要求の速度を制御することで、アプリの最適なパフォーマンスと応答性を維持し、ユーザー エクスペリエンスを向上させることができます。
  • Cost Management: 使用量に基づいてコストが発生するサービスの場合、レート制限は、処理される要求の量を制御することで、経費の管理と予測に役立ちます。

ASP.NET Core アプリにレート制限を実装すると、安定性、セキュリティ、パフォーマンスを維持するのに役立ちます。 その結果、すべてのユーザーに対して信頼性の高い効率的なサービスが提供されます。

DDoS 攻撃を防ぐ

レート制限は、要求の処理速度を制限することでサービス拒否 (DoS) 攻撃のリスクを軽減するのに役立ちますが、分散型サービス拒否 (DDoS) 攻撃の包括的なソリューションではありません。 DDoS 攻撃では、複数のシステムが多数の要求でアプリを圧倒し、レート制限だけでは処理が困難になります。

堅牢な DDoS 保護の場合は、商用 DDoS 保護サービスの使用を検討してください。 これらのサービスは、次のような高度な機能を提供します。

  • トラフィック分析: DDoS 攻撃をリアルタイムで検出して軽減するための受信トラフィックの継続的な監視と分析。
  • スケーラビリティ: 複数のサーバーとデータ センターにトラフィックを分散することで大規模な攻撃を処理する機能。
  • 自動化された軽減策: 手動による介入なしで悪意のあるトラフィックをすばやくブロックする自動応答メカニズム。
  • グローバル ネットワーク: ソースに近い攻撃を吸収して軽減するサーバーのグローバル ネットワーク。
  • 継続的な更新: 商用サービスは、新しい脅威や進化する脅威に適応するために、保護メカニズムを継続的に追跡および更新します。

クラウド ホスティング サービスを使用する場合、DDoS 保護は通常、ホスティング ソリューションの一部として使用できます。たとえば、Azure Web Application Firewall、AWS Shield 、Google Cloud Armor などです。 専用保護は、Web アプリケーション ファイアウォール (WAF) として、または Cloudflare や Akamai Kona Site Defender などの CDN ソリューションの一部として使用

商用 DDoS 保護サービスをレート制限と組み合わせて実装すると、包括的な防御戦略が提供され、アプリの安定性、セキュリティ、パフォーマンスが確保されます。

レート制限ミドルウェアを使用する

次の手順では、ASP.NET Core アプリでレート制限ミドルウェアを使用する方法を示します。

  1. レート制限サービスを構成します。

Program.cs ファイルで、適切なレート制限ポリシーを追加してレート制限サービスを構成します。 ポリシーをグローバル ポリシーまたは名前付きポリシーとして定義します。 次の例では、ユーザー (ID) またはグローバルに 1 分あたり 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");

インポート ファイル (_Imports.razor) を使用して、単一のルーティング可能なRazor コンポーネントまたはコンポーネントのフォルダーのポリシーを設定するには、ポリシー名で[EnableRateLimiting]属性を適用します。 次の例では、"override" という名前のレート制限ポリシーが適用されます。 このポリシーは、エンドポイントに現在適用されているポリシーを置き換えます。 グローバルリミッターは、この属性が適用されたエンドポイントで引き続き実行されます。

@page "/counter"
@using Microsoft.AspNetCore.RateLimiting
@attribute [EnableRateLimiting("override")]

<h1>Counter</h1>

が [EnableRateLimiting] で呼び出されない場合にのみ、RequireRateLimitingを、インポート ファイルを介してルーティング可能なコンポーネントまたはコンポーネントのフォルダーに適用します。

[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 を呼び出すことができます。

スライディングウィンドウリミッター

スライディングウィンドウアルゴリズム:

  • 固定時間枠リミッターに似ていますが、時間枠ごとにセグメントが追加されます。 ウィンドウは、セグメント間隔ごとに 1 つのセグメントをスライドします。 セグメント間隔は (時間枠の時間)/(1 つの時間枠あたりのセグメント数) です。
  • 1 つの時間枠の要求件数を permitLimit 件の要求に制限します。
  • 各時間枠は、時間枠あたり n セグメントに分割されます。
  • 1 つ前のウィンドウで期限切れとなった時間セグメントから取得した要求 (n セグメント前) は、現在のセグメントに追加されます。 1 つ前の時間枠で最も有効期限が切れている時間セグメントを、有効期限切れセグメントと呼びます。

30 秒の時間枠、1 時間枠あたり 3 つのセグメント、および 100 件の要求の制限を持つスライド式時間枠リミッターを示す次の表について考えてみましょう。

  • 上部の行と最初の列には、時間セグメントが表示されます。
  • 2 行目には、使用可能な残りの要求が表示されます。 残りの要求は、使用可能な要求から処理された要求とリサイクルされた要求を差し引いた値として計算されます。
  • 要求は、各時点で斜めの青い線に沿って移動します。
  • 時刻 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
60 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
60 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 を呼び出す必要があります。

コンカレンシー リミッター

コンカレンシー リミッターにより、同時実行要求の数は制限されます。 要求が 1 回行われると、コンカレンシーの制限が 1 つ減ります。 要求が完了すると、制限は 1 つ増やされます。 指定した期間の要求の合計数を制限する他の要求リミッターとは異なり、コンカレンシー リミッターは同時要求の数のみを制限し、一定期間の要求数の上限は制限しません。

次のコードでは、コンカレンシー リミッターを使用します:

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();

レート制限パーティション

レート制限パーティションは、トラフィックを個別のバケットに分割し、それぞれが独自のレート制限カウンターを取得します。 この方法では、1 つのグローバル カウンターよりも詳細な制御が提供されます。 ユーザー ID、IP アドレス、API キーなど、さまざまなキーによってパーティション バケットが定義されます。

パーティション分割のメリット

  • 公平性: 1 人のユーザーが全員のレート制限全体を使用することはできません。
  • 粒度: ユーザーとリソースごとに異なる制限。
  • セキュリティ: 標的型の不正使用に対する保護を強化します。
  • 階層化されたサービス: 制限が異なるサービス レベルのサポート。

パーティション分割されたレート制限により、公平なリソース割り当てを確保しながら、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)
        }));

ユーザー ID 別

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)
        });
});

チェーン リミッタを作成する

CreateChained API は、複数のPartitionedRateLimiter インスタンスを受け入れ、それらを 1 つのPartitionedRateLimiterに結合します。 結合されたリミッターは、すべての入力リミッターを順番に実行します。 結果はGlobalLimiterに割り当てられたPartitionedRateLimiterであるため、チェーンはすべてのエンドポイントに適用されます。 代わりに特定のエンドポイントのリミッタをチェーンするには、名前付きポリシーの チェーンリミッタに示すように、名前付きポリシーを使用します。

次のコードでは 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 を返します。これは、名前付きポリシーのパーティション ファクトリに必要な戻り値の型です。

次の "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 に設定し、1 つの共有タイマーからすべてのリミッタを補充します。
  • 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) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;

        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((long)Math.Ceiling(retryAfter.TotalSeconds)).ToString(NumberFormatInfo.InvariantInfo);
        }

        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);
    };
});

この例では、 using System.Globalization; と using System.Threading.RateLimiting;が必要です。

Note

FixedWindowRateLimiter は、次の固定ウィンドウ境界に基づいて MetadataName.RetryAfter を報告します。 OnRejected でこのメタデータを Retry-After 応答ヘッダーにコピーするアプリは、正確な再試行間隔を自動的に返します。 ConcurrencyLimiterなど、許可が利用可能なタイミングを見積もることができないリミッタは、RetryAfterメタデータを提供しません。

もう 1 つのオプションは、要求をキューに入れます。

リクエストのキューイング

キューを有効にすると、要求がレート制限を超えると、システムによってキューに配置されます。 許可が使用可能になるかタイムアウトが発生するまで、要求はキュー内で待機します。 システムは、構成可能なキューの順序に従って要求を処理します。

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 属性

コントローラー、アクション メソッド、または Razor Page に[EnableRateLimiting]属性と[DisableRateLimiting]属性を適用します。 Razor ページの場合は、ページ ハンドラーではなく、Razor ページに属性を適用します。 たとえば、OnGet、OnPost、またはその他のページ ハンドラーに[EnableRateLimiting]を適用することはできません。

[DisableRateLimiting]属性は、名前付きレート リミッタまたはグローバル リミッタが適用されているかどうかにかかわらず、コントローラー、アクション メソッド、または Razor Page のレート制限を無効にします。 たとえば、すべてのコントローラー エンドポイントに 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) テスト スクリプト レコーダーなどのツールを使用して JMeter スクリプトを作成し、スクリプトをAzure Load Testingに読み込みます。

ユーザー入力を使用してパーティションを作成すると、アプリは サービス拒否 (DoS) 攻撃に対して脆弱になります。 たとえば、クライアント IP アドレスを使用してパーティションを作成すると、アプリは IP ソース アドレススプーフィングを使用するサービス拒否攻撃に対して脆弱になります。 詳細については、『BCP 38 RFC 2827 ネットワーク イングレス フィルタリング: IP ソース アドレス スプーフィングを使用するサービス拒否攻撃の無効化』を参照してください。