Middleware mit Bandbreitenbegrenzung in ASP.NET Core

Von Arvin Kahbazi, Maarten Balliauw und Rick Anderson

Die Microsoft.AspNetCore.RateLimiting-Middleware bietet Middleware mit Bandbreitenbegrenzung. Apps konfigurieren Richtlinien zur Begrenzung der Bandbreite und wenden dann die Richtlinien auf Endpunkten an. Apps mit Ratenbegrenzung sollten vor der Bereitstellung sorgfältigen Auslastungstests und Überprüfungen unterzogen werden. Weitere Informationen finden Sie unter Testen von Endpunkten mit Ratenbegrenzung in diesem Artikel.

Eine Einführung in die Ratelimitierung finden Sie unter Middleware mit Bandbreitenbegrenzung.

Ratenbegrenzungsalgorithmen

Die RateLimiterOptionsExtensions-Klasse stellt die folgenden Erweiterungsmethoden für die Ratenbegrenzung bereit:

Begrenzung mit festem Zeitfenster

Die AddFixedWindowLimiter-Methode verwendet ein festes Zeitfenster, um Anforderungen einzuschränken. Wenn das Zeitfenster abläuft, wird ein neues Zeitfenster gestartet, und die Anforderungsbegrenzung wird zurückgesetzt.

Betrachten Sie folgenden Code:

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

Der vorangehende Code:

  • AddRateLimiter wird aufgerufen, um der Dienstsammlung einen Ratenbegrenzungsdienst hinzuzufügen.
  • AddFixedWindowLimiter wird aufgerufen, um eine Begrenzung mit festem Zeitfenster mit dem Richtliniennamen "fixed" zu erstellen, und Folgendes wird festgelegt:
  • PermitLimit auf 4 und Window für die Zeit auf 12. Es sind maximal 4 Anforderungen pro 12-Sekunden-Fenster zulässig.
  • QueueProcessingOrder in OldestFirst.
  • QueueLimit auf 2.
  • UseRateLimiter wird aufgerufen, um die Ratenbegrenzung zu aktivieren.

Apps sollten die Konfiguration verwenden, um Begrenzungsoptionen festzulegen. Der folgende Code aktualisiert den vorherigen Code mithilfe von MyRateLimitOptions für die Konfiguration:

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

UseRateLimiter muss nach UseRouting aufgerufen werden, wenn endpunktspezifische APIs für die Ratenbegrenzung verwendet werden. Wenn zum Beispiel das Attribut [EnableRateLimiting] verwendet wird, muss UseRateLimiter nach UseRouting aufgerufen werden. Wenn nur globale Begrenzungen aufgerufen werden, UseRateLimiter kann vor UseRouting aufgerufen werden.

Begrenzung mit gleitendem Zeitfenster

Für einen Algorithmus mit gleitendem Zeitfenster gilt Folgendes:

  • Er ähnelt der Begrenzung mit festem Zeitfenster, fügt jedoch Segmente pro Fenster hinzu. Das Fenster wird in jedem Segmentintervall um ein Segment versetzt. Das Segmentintervall ist (Zeitfenster)/(Segmente pro Fenster).
  • Es schränkt die Anforderungen für ein Fenster auf permitLimit Anforderungen ein.
  • Jedes Zeitfenster ist in n Segmente pro Fenster unterteilt.
  • Anforderungen aus dem abgelaufenen Zeitsegment im vorangegangenen Zeitfenster (n Segmente vor dem aktuellen Segment) werden dem aktuellen Segment hinzugefügt. Wir bezeichnen das am frühesten abgelaufene Zeitsegment aus dem vorherigen Fenster als abgelaufenes Segment.

Sehen Sie sich die folgende Tabelle an, die eine Begrenzung mit gleitendem Zeitfenster mit einem Fenster von 30 Sekunden, drei Segmenten pro Fenster und einem Grenzwert von 100 Anforderungen zeigt:

  • Die oberste Zeile und erste Spalte zeigen das Zeitsegment an.
  • Die zweite Zeile zeigt die restlichen verfügbaren Anforderungen an. Die verbleibenden Anforderungen werden folgendermaßen berechnet: verfügbare Anforderungen minus verarbeitete Anforderungen plus wiederverwendete Anforderungen.
  • Anforderungen für die einzelnen Zeitangaben bewegen sich jeweils entlang der diagonalen blauen Linie.
  • Ab der Zeit 30 wird die Anforderung aus dem abgelaufenen Zeitsegment wieder dem Anforderungslimit hinzugefügt, wie in den roten Linien dargestellt.

Table showing requests, limits, and recycled slots

Die folgende Tabelle zeigt die Daten im vorherigen Diagramm in einem anderen Format. In der Spalte Verfügbar werden die Anforderungen angezeigt, die aus dem vorherigen Segment verfügbar sind (der Übertrag aus der vorherigen Zeile). Die erste Zeile zeigt 100 verfügbare Anforderungen an, da kein vorheriges Segment vorhanden ist.

Zeit Verfügbar Verwendet Aus abgelaufenen wiederverwendet Übertrag
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

Der folgende Code verwendet die Ratenbegrenzung mit gleitendem Zeitfenster:

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

Begrenzung mit Tokenbucket

Die Tokenbucketbegrenzung ähnelt der Begrenzung mit gleitendem Zeitfenster. Statt jedoch die Anforderungen aus dem abgelaufenen Segment wieder hinzuzufügen, wird in jedem Auffüllungszeitraum eine feste Anzahl von Token hinzugefügt. Die in den einzelnen Segmenten hinzugefügten Token können die Anzahl verfügbarer Token nicht auf eine Zahl erhöhen, die die Tokenbucketbegrenzung übersteigt. Die folgende Tabelle zeigt eine Tokenbucketbegrenzung mit einem Grenzwert von 100 Token und einem Auffüllungszeitraum von 10 Sekunden.

Zeit Verfügbar Verwendet Hinzugefügt Übertrag
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

Der folgende Code verwendet die Tokenbucketbegrenzung:

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

Wenn AutoReplenishment auf truefestgelegt ist, füllt ein interner Timer die Token in jeder ReplenishmentPeriod auf. Wenn die Option auf false festgelegt ist, muss die App TryReplenish für die Begrenzung aufrufen.

Parallelitätsbegrenzung

Die Parallelitätsbegrenzung schränkt die Anzahl gleichzeitiger Anforderungen ein. Durch jede Anforderung wird die Parallelitätsbegrenzung um eins reduziert. Wenn eine Anforderung abgeschlossen ist, wird die Begrenzung um eins erhöht. Im Gegensatz zu anderen Anforderungsbegrenzungen, die die Gesamtanzahl von Anforderungen für einen angegebenen Zeitraum begrenzen, schränkt die Parallelitätsbegrenzung nur die Anzahl gleichzeitiger Anforderungen ein und begrenzt nicht die Anzahl der Anforderungen in einem bestimmten Zeitraum.

Der folgende Code verwendet die Parallelitätsbegrenzung:

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

Erstellen verketteter Begrenzungen

Die CreateChained-API ermöglicht das Übergeben mehrerer PartitionedRateLimiter, die zu einem PartitionedRateLimiter kombiniert werden. Die kombinierte Begrenzung führt alle Eingabebegrenzungen nacheinander aus.

Der folgende Code verwendet CreateChained:

using System.Globalization;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(_ =>
{
    _.OnRejected = (context, _) =>
    {
        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;
        context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");

        return new ValueTask();
    };
    _.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();

Weitere Informationen finden Sie im CreateChained-Quellcode.

EnableRateLimiting- und DisableRateLimiting-Attribute

Die Attribute [EnableRateLimiting] und [DisableRateLimiting] können auf einen Controller, eine Aktionsmethode oder eine Razor-Seite angewendet werden. Für Razor Pages muss das Attribut auf die Razor-Seite und nicht auf die Seitenhandler angewendet werden. Beispielsweise kann [EnableRateLimiting] nicht auf OnGet, OnPost oder andere Seitenhandler angewendet werden.

Das [DisableRateLimiting]-Attribut deaktiviert die Ratenbegrenzung für den Controller, die Aktionsmethode oder die Razor-Seite, unabhängig von der Anwendung benannter Ratenbegrenzungen oder globaler Begrenzungen. Betrachten Sie beispielsweise den folgenden Code, der RequireRateLimiting aufruft, um die fixedPolicy-Ratenbegrenzung auf alle Controllerendpunkte anzuwenden:

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

Im folgenden Code deaktiviert [DisableRateLimiting] die Ratenbegrenzung und setzt [EnableRateLimiting("fixed")] außer Kraft, das auf Home2Controller und app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) angewendet wird, die in Program.cs aufgerufen werden:

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

Im vorherigen Code wird [EnableRateLimiting("sliding")]nicht auf die Privacy-Aktionsmethode angewendet, da app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) in Program.cs aufgerufen wurde.

Betrachten Sie den folgenden Code, der RequireRateLimiting nicht für MapRazorPages oder MapDefaultControllerRoute aufruft:

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

Betrachten Sie den folgenden Controller:

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

Im oben aufgeführten Controller gilt Folgendes:

  • Die Richtlinienratenbegrenzung "fixed" wird auf alle Aktionsmethoden angewendet, die nicht über die Attribute EnableRateLimiting und DisableRateLimiting verfügen.
  • Die Richtlinienratenbegrenzung "sliding" wird auf die Privacy-Aktion angewendet.
  • Die Ratenbegrenzung wird für die NoLimit-Aktionsmethode deaktiviert.

Anwenden von Attributen auf Razor Pages

Für Razor Pages muss das Attribut auf die Razor-Seite und nicht auf die Seitenhandler angewendet werden. Beispielsweise kann [EnableRateLimiting] nicht auf OnGet, OnPost oder andere Seitenhandler angewendet werden.

Das Attribut DisableRateLimiting deaktiviert die Ratenbegrenzung für eine Razor-Seite. EnableRateLimiting wird nur dann auf eine Razor-Seite angewendet, wenn MapRazorPages().RequireRateLimiting(Policy)nicht aufgerufen wurde.

Vergleich von Begrenzungsalgorithmen

Die Begrenzungen mit festem Zeitfenster, gleitendem Zeitfenster und Tokenbucket begrenzen alle die maximale Anzahl von Anforderungen in einem bestimmten Zeitraum. Die Parallelitätsbegrenzung schränkt nur die Anzahl gleichzeitiger Anforderungen ein und begrenzt nicht die Gesamtanzahl der Anforderungen in einem Zeitraum. Die Kosten eines Endpunkts sollten bei der Auswahl einer Begrenzung berücksichtigt werden. Die Kosten eines Endpunkts umfassen die verwendeten Ressourcen, z. B. Zeit, Datenzugriff, CPU und E/A.

Beispiele für Ratenbegrenzungen

Die folgenden Beispiele sind nicht für Produktionscode bestimmt, sondern Beispiele für die Verwendung der Begrenzungen.

Begrenzung mit OnRejected, RetryAfter und GlobalLimiter

Im folgenden Beispiel geschieht Folgendes:

  • Ein RateLimiterOptions.OnRejected-Rückruf wird erstellt. Dieser wird aufgerufen, wenn eine Anforderung den angegebenen Grenzwert überschreitet. retryAfter kann mit den Begrenzungen TokenBucketRateLimiter, FixedWindowLimiter und SlidingWindowLimiter verwendet werden, weil diese Algorithmen schätzen können, wann weitere Genehmigungen hinzugefügt werden. Der ConcurrencyLimiter hat keine Möglichkeit zu berechnen, wann Genehmigungen verfügbar sein werden.

  • Folgende Begrenzungen werden hinzugefügt:

    • Eine SampleRateLimiterPolicy, die die IRateLimiterPolicy<TPartitionKey>-Schnittstelle implementiert. Die SampleRateLimiterPolicy-Klasse wird weiter unten in diesem Artikel gezeigt.
    • Für eine SlidingWindowLimiter gilt:
      • Mit je einer Partition für die einzelnen authentifizierten Benutzer*innen.
      • Mit einer gemeinsamen Partition für alle anonymen Benutzer*innen.
    • Ein GlobalLimiter, der auf alle Anforderungen angewendet wird. Die globale Begrenzung wird zuerst ausgeführt, gefolgt von der endpunktspezifischen Begrenzung, sofern vorhanden. Der GlobalLimiter erstellt eine Partition für jede IPAddress.
// Preceding code removed for brevity.
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
    throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.Configure<MyRateLimitOptions>(
    builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var userPolicyName = "user";
var helloPolicy = "hello";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

builder.Services.AddRateLimiter(limiterOptions =>
{
    limiterOptions.OnRejected = (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;
        context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
            .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
            .LogWarning("OnRejected: {GetUserEndPoint}", GetUserEndPoint(context.HttpContext));

        return new ValueTask();
    };

    limiterOptions.AddPolicy<string, SampleRateLimiterPolicy>(helloPolicy);
    limiterOptions.AddPolicy(userPolicyName, context =>
    {
        var username = "anonymous user";
        if (context.User.Identity?.IsAuthenticated is true)
        {
            username = context.User.ToString()!;
        }

        return RateLimitPartition.GetSlidingWindowLimiter(username,
            _ => new SlidingWindowRateLimiterOptions
            {
                PermitLimit = myOptions.PermitLimit,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = myOptions.QueueLimit,
                Window = TimeSpan.FromSeconds(myOptions.Window),
                SegmentsPerWindow = myOptions.SegmentsPerWindow
            });

    });
    
    limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, IPAddress>(context =>
    {
        IPAddress? remoteIpAddress = context.Connection.RemoteIpAddress;

        if (!IPAddress.IsLoopback(remoteIpAddress!))
        {
            return RateLimitPartition.GetTokenBucketLimiter
            (remoteIpAddress!, _ =>
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = myOptions.TokenLimit2,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = myOptions.QueueLimit,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                    TokensPerPeriod = myOptions.TokensPerPeriod,
                    AutoReplenishment = myOptions.AutoReplenishment
                });
        }

        return RateLimitPartition.GetNoLimiter(IPAddress.Loopback);
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseRateLimiter();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages().RequireRateLimiting(userPolicyName);
app.MapDefaultControllerRoute();

static string GetUserEndPoint(HttpContext context) =>
   $"User {context.User.Identity?.Name ?? "Anonymous"} endpoint:{context.Request.Path}"
   + $" {context.Connection.RemoteIpAddress}";
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
    .RequireRateLimiting(userPolicyName);

app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
    .RequireRateLimiting(helloPolicy);

app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}");

app.Run();

Warnung

Das Erstellen von Partitionen für Client-IP-Adressen macht die App anfällig für Denial-of-Service-Angriffe, die IP-Quelladressen-Spoofing verwenden. Weitere Informationen finden Sie unter BCP 38 RFC 2827 Netzwerkeingangsfilterung: Abwehr von Denial-of-Service-Angriffen mit IP-Quelladressen-Spoofing.

Die vollständige Program.cs-Datei finden Sie im Beispielrepository.

Die Klasse SampleRateLimiterPolicy

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using WebRateLimitAuth.Models;

namespace WebRateLimitAuth;

public class SampleRateLimiterPolicy : IRateLimiterPolicy<string>
{
    private Func<OnRejectedContext, CancellationToken, ValueTask>? _onRejected;
    private readonly MyRateLimitOptions _options;

    public SampleRateLimiterPolicy(ILogger<SampleRateLimiterPolicy> logger,
                                   IOptions<MyRateLimitOptions> options)
    {
        _onRejected = (ctx, token) =>
        {
            ctx.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            logger.LogWarning($"Request rejected by {nameof(SampleRateLimiterPolicy)}");
            return ValueTask.CompletedTask;
        };
        _options = options.Value;
    }

    public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected => _onRejected;

    public RateLimitPartition<string> GetPartition(HttpContext httpContext)
    {
        return RateLimitPartition.GetSlidingWindowLimiter(string.Empty,
            _ => new SlidingWindowRateLimiterOptions
            {
                PermitLimit = _options.PermitLimit,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = _options.QueueLimit,
                Window = TimeSpan.FromSeconds(_options.Window),
                SegmentsPerWindow = _options.SegmentsPerWindow
            });
    }
}

Im vorherigen Code verwendet OnRejected die Funktion OnRejectedContext, um den Antwortstatus auf 429 Zu viele Anforderungen festzulegen. Der standardmäßige Ablehnungsstatus lautet 503 Dienst nicht verfügbar.

Begrenzung mit Autorisierung

Das folgende Beispiel verwendet JSON Web Tokens (JWT) und erstellt eine Partition mit dem JWT-Zugriffstoken. In einer Produktions-App wird das JWT in der Regel von einem Server bereitgestellt, der als Sicherheitstokendienst (Security Token Service, STS) fungiert. Bei der lokalen Entwicklung kann das Befehlszeilentool dotnet user-jwts verwendet werden, um App-spezifische lokale JWTs zu erstellen und zu verwalten.

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Primitives;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var jwtPolicyName = "jwt";

builder.Services.AddRateLimiter(limiterOptions =>
{
    limiterOptions.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    limiterOptions.AddPolicy(policyName: jwtPolicyName, partitioner: httpContext =>
    {
        var accessToken = httpContext.Features.Get<IAuthenticateResultFeature>()?
                              .AuthenticateResult?.Properties?.GetTokenValue("access_token")?.ToString()
                          ?? string.Empty;

        if (!StringValues.IsNullOrEmpty(accessToken))
        {
            return RateLimitPartition.GetTokenBucketLimiter(accessToken, _ =>
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = myOptions.TokenLimit2,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = myOptions.QueueLimit,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                    TokensPerPeriod = myOptions.TokensPerPeriod,
                    AutoReplenishment = myOptions.AutoReplenishment
                });
        }

        return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>
            new TokenBucketRateLimiterOptions
            {
                TokenLimit = myOptions.TokenLimit,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = myOptions.QueueLimit,
                ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                TokensPerPeriod = myOptions.TokensPerPeriod,
                AutoReplenishment = true
            });
    });
});

var app = builder.Build();

app.UseAuthorization();
app.UseRateLimiter();

app.MapGet("/", () => "Hello, World!");

app.MapGet("/jwt", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
    .RequireRateLimiting(jwtPolicyName)
    .RequireAuthorization();

app.MapPost("/post", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
    .RequireRateLimiting(jwtPolicyName)
    .RequireAuthorization();

app.Run();

static string GetUserEndPointMethod(HttpContext context) =>
    $"Hello {context.User.Identity?.Name ?? "Anonymous"} " +
    $"Endpoint:{context.Request.Path} Method: {context.Request.Method}";

Begrenzung mit ConcurrencyLimiter, TokenBucketRateLimiter und Autorisierung

Im folgenden Beispiel geschieht Folgendes:

  • Ein ConcurrencyLimiter wird mit dem Richtliniennamen "get" hinzugefügt, der auf den Razor-Seiten verwendet wird.
  • Ein TokenBucketRateLimiter wird mit je einer Partition für die einzelnen autorisierten Benutzer*innen und einer Partition für alle anonymen Benutzer*innen hinzugefügt.
  • RateLimiterOptions.RejectionStatusCode wird auf 429 Zu viele Anforderungen festgelegt.
var getPolicyName = "get";
var postPolicyName = "post";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

builder.Services.AddRateLimiter(_ => _
    .AddConcurrencyLimiter(policyName: getPolicyName, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    })
    .AddPolicy(policyName: postPolicyName, partitioner: httpContext =>
    {
        string userName = httpContext.User.Identity?.Name ?? string.Empty;

        if (!StringValues.IsNullOrEmpty(userName))
        {
            return RateLimitPartition.GetTokenBucketLimiter(userName, _ =>
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = myOptions.TokenLimit2,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = myOptions.QueueLimit,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                    TokensPerPeriod = myOptions.TokensPerPeriod,
                    AutoReplenishment = myOptions.AutoReplenishment
                });
        }

        return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>
            new TokenBucketRateLimiterOptions
            {
                TokenLimit = myOptions.TokenLimit,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = myOptions.QueueLimit,
                ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                TokensPerPeriod = myOptions.TokensPerPeriod,
                AutoReplenishment = true
            });
    }));

Die vollständige Program.cs-Datei finden Sie im Beispielrepository.

Testen von Endpunkten mit Ratenbegrenzung

Bevor Sie eine App mit Ratenbegrenzung in der Produktion bereitstellen, testen Sie die App, um die verwendeten Ratenbegrenzungen und -optionen zu validieren. Erstellen Sie beispielsweise ein JMeter-Skript mit einem Tool wie BlazeMeter oder Apache JMeter HTTP(S) Test Script Recorder, und laden Sie das Skript in Azure Load Testing.

Das Erstellen von Partitionen mit Benutzereingabe macht die App anfällig für Denial-of-Service-Angriffe (DoS). Durch das Erstellen von Partitionen für Client-IP-Adressen wird die App beispielsweise anfällig für Denial-of-Service-Angriffe, die IP-Quelladressen-Spoofing verwenden. Weitere Informationen finden Sie unter BCP 38 RFC 2827 Netzwerkeingangsfilterung: Abwehr von Denial-of-Service-Angriffen mit IP-Quelladressen-Spoofing.

Zusätzliche Ressourcen