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.

Gründe für die Verwendung von Ratenbeschränkungen

Die Häufigkeitsbeschränkung kann zum Verwalten des Flusses eingehender Anforderungen an eine App verwendet werden. Wichtige Gründe für die Implementierung der Zinsbegrenzung:

  • Missbrauch verhindern: Die Beschränkung der Rate trägt dazu bei, eine App vor Missbrauch zu schützen, indem die Anzahl der Anforderungen beschränkt wird, die ein Benutzer oder Client in einem bestimmten Zeitraum vornehmen kann. Dieser Schutz ist besonders wichtig für öffentliche APIs.

  • Gewährleistung der fairen Nutzung: Indem Sie Grenzwerte festlegen, die verhindern, dass Benutzer das System monopolisieren, stellen Sie sicher, dass alle Benutzer fairen Zugriff auf Ressourcen haben.

  • Schützen von Ressourcen: Durch das Einschränken von Raten wird verhindert, dass serverüberlastet ist, indem die Anzahl der Anforderungen gesteuert wird, die verarbeitet werden können. Sie schützt die Back-End-Ressourcen davor, überfordert zu werden.

  • Verbesserung der Sicherheit: Sie kann das Risiko von Denial of Service -Angriffen (DoS) verringern, indem sie die Rate einschränken, mit der Anforderungen verarbeitet werden. Es erschwert Angreifern, ein System zu überfluten.

  • Verbesserung der Leistung: Durch die Steuerung der Häufigkeit eingehender Anforderungen können Sie eine optimale Leistung und Reaktionsfähigkeit einer App gewährleisten und eine bessere Benutzererfahrung gewährleisten.

  • Kostenmanagement: Bei Diensten, die auf der Grundlage der Nutzung kostenaufwendigen, kann die Zinsbegrenzung dazu beitragen, Ausgaben zu verwalten und vorherzusagen, indem das Volumen der verarbeiteten Anforderungen gesteuert wird.

Die Implementierung von Ratenbeschränkungen in einer ASP.NET Core-App kann dazu beitragen, Stabilität, Sicherheit und Leistung aufrechtzuerhalten. Das Ergebnis ist ein zuverlässiger und effizienter Service für alle Benutzer.

Verhindern von DDoS-Angriffen

Während die Zinsbegrenzung dazu beitragen kann, das Risiko von Denial of Service -Angriffen (DoS) zu verringern, indem die Rate beschränkt wird, mit der Anforderungen verarbeitet werden, ist es keine umfassende Lösung für DDoS-Angriffe (Distributed Denial of Service). DDoS-Angriffe umfassen mehrere Systeme, die eine App mit einer Flut von Anforderungen überwältigen, was es schwierig macht, mit der Ratenbegrenzung allein zu umgehen.

Für einen robusten DDoS-Schutz sollten Sie einen kommerziellen DDoS-Schutzdienst verwenden. Diese Dienste bieten erweiterte Features wie:

  • Datenverkehrsanalyse: Kontinuierliche Überwachung und Analyse eingehender Datenverkehr zur Erkennung und Entschärfung von DDoS-Angriffen in Echtzeit.
  • Skalierbarkeit: Die Möglichkeit, große Angriffe zu bewältigen, indem der Datenverkehr über mehrere Server und Rechenzentren verteilt wird.
  • Automatisierte Entschärfung: Automatisierte Reaktionsmechanismen, um bösartigen Datenverkehr ohne manuelles Eingreifen schnell zu blockieren.
  • Globales Netzwerk: Ein globales Netzwerk von Servern, um Angriffe zu absorbieren und zu mindern, die der Quelle näher sind.
  • Ständige Updates: Kommerzielle Dienste verfolgen und aktualisieren kontinuierlich ihre Schutzmechanismen, um sich an neue und sich entwickelnde Bedrohungen anzupassen.

Bei Verwendung eines Cloudhostingdiensts ist DDoS-Schutz in der Regel als Teil der Hostinglösung verfügbar, z. B. Azure Web Application Firewall, AWS Shield oder Google Cloud Armor. Dedizierte Schutzmaßnahmen sind als Webanwendungsfirewalls (WAF) oder als Teil einer CDN-Lösung wie Cloudflare oder Akamai Kona Site Defender verfügbar.

Die Implementierung eines kommerziellen DDoS-Schutzdiensts in Verbindung mit der Zinsbegrenzung kann eine umfassende Verteidigungsstrategie bieten, um die Stabilität, Sicherheit und Leistung einer App sicherzustellen.

Verwenden Sie Middleware zur Anfragenbegrenzung

Die folgenden Schritte zeigen, wie Sie die Ratelimitierung von Middleware in einer ASP.NET Core-App verwenden:

  1. Konfigurieren Sie Dienste zur Begrenzung der Rate.

Konfigurieren Sie in der Program.cs Datei die Dienste zum Einschränken von Raten, indem Sie die entsprechenden Richtlinien für die Ratenbegrenzung hinzufügen. Definieren Sie Richtlinien als globale oder benannte Richtlinien. Im folgenden Beispiel werden 10 Anforderungen pro Minute nach Benutzer (Identität) oder global zulässig:

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

Benannte Richtlinien müssen explizit auf die Seiten oder Endpunkte angewendet werden. Im folgenden Beispiel wird eine Richtlinie zur Begrenzung mit festem Fenster mit dem Namen "fixed" hinzugefügt, die Sie später einem Endpunkt hinzufügen:

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

Der globale Grenzwert gilt automatisch für alle Endpunkte, wenn Sie ihn über Optionen konfigurieren . GlobalLimiter.

  1. Aktivierung von Middleware zur Ratenbegrenzung

    Aktivieren Sie in der Program.cs Datei die Ratelimitierung der Middleware durch Aufrufen von UseRateLimiter:

app.UseRouting();

app.UseRateLimiter();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

app.Run();

Richtlinien zur Ratenbegrenzung auf Endpunkte oder Seiten anwenden

Anwenden von Ratenbeschränkungen auf Web-API-Endpunkte

Wenden Sie eine benannte Richtlinie auf den Endpunkt oder die Gruppe an, z. B.:


app.MapGet("/api/resource", () => "This endpoint is rate limited")
   .RequireRateLimiting("fixed"); // Apply specific policy to an endpoint

Anwenden von Ratenbeschränkungen auf MVC-Controller

Wenden Sie die konfigurierten Richtlinien zur Begrenzung der Rate auf bestimmte Endpunkte oder global an. Zum Beispiel, um die Richtlinie "fest" auf alle Controller-Endpunkte anzuwenden:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers().RequireRateLimiting("fixed");
});

Anwenden von Ratenbeschränkungen auf serverseitige Blazor-Apps

Um die Ratenbegrenzung für alle routingfähigen Razor Komponenten der App festzulegen, geben Sie RequireRateLimiting mit dem Namen der Richtlinie zur Ratenbegrenzung beim Aufruf MapRazorComponents in der Program-Datei an. Im folgenden Beispiel wird die Richtlinie zur Begrenzung der Rate mit dem Namen "policy" angewendet:

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .RequireRateLimiting("policy");

Um eine Richtlinie für eine einzelne routingfähige Razor Komponente oder einen Ordner mit Komponenten über eine Importdatei (_Imports.razor) festzulegen, wenden Sie das [EnableRateLimiting] Attribut mit dem Richtliniennamen an. Im folgenden Beispiel wird die Richtlinie zur Begrenzung der Rate mit dem Namen "override" angewendet. Die Richtlinie ersetzt alle Richtlinien, die derzeit auf den Endpunkt angewendet werden. Der globale Grenzwert wird weiterhin auf dem Endpunkt ausgeführt, auf den dieses Attribut angewendet wurde.

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

<h1>Counter</h1>

Wenden Sie das [EnableRateLimiting]-Attribut nur auf eine routingfähige Komponente oder auf einen Ordner von Komponenten mithilfe einer Importdatei an, wenn RequireRateLimitingnicht auf MapRazorComponents aufgerufen wird.

Verwenden Sie das [DisableRateLimiting] Attribut , um die Ratebegrenzung für eine routingfähige Komponente oder einen Ordner mit Komponenten über eine Importdatei zu deaktivieren.

Ratenbegrenzungsalgorithmen

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

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. Berücksichtigen Sie die Kosten eines Endpunkts, wenn Sie einen Grenzwert auswählen. Die Kosten eines Endpunkts umfassen die verwendeten Ressourcen, z. B. Zeit, Datenzugriff, CPU und E/A.

Festfensterbegrenzer

Die AddFixedWindowLimiter-Methode verwendet ein festes Zeitfenster, um Anforderungen einzuschränken. Wenn das Zeitfenster abläuft, wird ein neues Zeitfenster gestartet und der Anforderungsgrenzwert 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 einen festen Fensterbegrenzer mit dem Richtliniennamen "fixed" zu erstellen und setzt Folgendes fest:
  • PermitLimit auf 4 und Window für die Zeit auf 12. Es sind maximal 4 Anforderungen pro 12-Sekunden-Fenster zulässig.
  • QueueProcessingOrder auf OldestFirst.
  • QueueLimit auf 2 (legen Sie diesen auf 0 fest, um den Warteschlangenmechanismus zu deaktivieren).
  • 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.

Gleitende Fensterbegrenzung

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 immer um ein Segment verschoben. 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.

Tabelle mit Anforderungen, Limits und wiederverwendeten 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();

Ratenbegrenzungspartitionen

Rate-Limiting-Partitionen unterteilen den Datenverkehr in separate Buckets, die jeweils über eigene Zähler für die Ratenbegrenzung verfügen. Dieser Ansatz bietet eine präzisere Kontrolle als ein einzelner globaler Zähler. Verschiedene Schlüssel, z. B. Benutzer-ID, IP-Adresse oder API-Schlüssel, definieren die Partitions-Buckets.

Vorteile der Partitionierung

  • Fairness: Ein Benutzer kann das gesamte Preislimit für jeden nicht nutzen.
  • Granularität: Unterschiedliche Grenzwerte für verschiedene Benutzer und Ressourcen.
  • Sicherheit: Besserer Schutz vor gezielten Missbrauch.
  • Mehrstufiger Dienst: Unterstützung für Dienstebenen mit unterschiedlichen Grenzwerten.

Durch partitionierte Ratenbeschränkungen können Sie präzise steuern, wie Sie API-Datenverkehr verwalten und gleichzeitig eine faire Ressourcenzuordnung sicherstellen.

Nach IP-Adresse

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 50,
            Window = TimeSpan.FromMinutes(1)
        }));

Nach Benutzeridentität

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.User.Identity?.Name ?? "anonymous",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100,
            Window = TimeSpan.FromMinutes(1)
        }));

Nach API-Schlüssel

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

Nach Endpunkt-Pfad

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

Erstellen verketteter Begrenzungen

Die CreateChained API akzeptiert mehrere PartitionedRateLimiter Instanzen und kombiniert sie in einer PartitionedRateLimiter. Der kombinierte Begrenzer läuft alle Eingabebegrenzer nacheinander durch. Da das Ergebnis einem PartitionedRateLimiter zugewiesen GlobalLimiterist, gilt die Kette für jeden Endpunkt. Wenn Sie stattdessen Begrenzungen für einen bestimmten Endpunkt verketten möchten, verwenden Sie eine benannte Richtlinie, wie in "Chain limiters" in einer benannten Richtlinie dargestellt.

Der folgende Code verwendet 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();

Weitere Informationen finden Sie im CreateChained-Quellcode.

Verkettengrenzer in einer benannten Richtlinie

CreateChained verkettet globale Limiter, die auf jeden Endpunkt angewendet werden. Um mehrere Limitertypen zu kombinieren und sie auf bestimmte Endpunkte zu beschränken, verketten Sie die Limiter innerhalb einer benannten Richtlinie mit CreateChained. Diese Überladung gibt einen einzelnen RateLimiter zurück, der jeden Limiter in Sequenz ausführt. Dabei handelt es sich um den Rückgabetyp, den die Partitions-Factory einer benannten Richtlinie erfordert.

Die folgende "combined" Richtlinie verkettet einen Token-Bucket-Limiter und einen Parallelitätsbegrenzer mit RateLimiter.CreateChained und wendet sie dann mit RequireRateLimiting auf einen einzelnen Endpunkt an:

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

Eine Anfrage muss von jedem Limiter in der Kette eine Lease erhalten, um fortzufahren, und die Limiter werden in der Reihenfolge ausgeführt, in der sie an RateLimiter.CreateChained übergeben werden. Wenn ein Limiter die Anforderung ablehnt, wird die Anforderung abgelehnt, und die bereits von früheren Limitierern in der Kette erworbenen Leases werden in umgekehrter Reihenfolge verworfen.

Beachten Sie beim Verketten von Limitern in einer benannten Richtlinie Folgendes:

  • Durch das Freigeben eines Mietvertrags wird die Berechtigung für einen Parallelitätsbegrenzer zurückgegeben. Die zeitbasierten Begrenzungen (Token-Bucket, festes Fenster und Gleitfenster) geben keine Genehmigung zurück, die bereits erworben wurde, wenn ein späterer Limiter in der Kette die Anforderung ablehnt. Berücksichtigen Sie dies also beim Sortieren der Begrenzungen in der Kette.
  • Wird ein TokenBucketRateLimiter direkt mit auf true gesetztem AutoReplenishment erstellt, erhält jede Limiterinstanz ihren eigenen Timer. Die AddTokenBucketLimiter- und RateLimitPartition.GetTokenBucketLimiter-Helfer setzen stattdessen AutoReplenishment auf false und füllen alle ihre Limiter aus einem einzigen gemeinsamen Timer wieder auf.
  • RateLimiter.CreateChained gibt die übergebenen Begrenzer nicht frei. Im vorherigen Beispiel zwischenspeichert die Partition den verketteten Begrenzer, und das Framework verwaltet dessen Lebensdauer. Wenn Sie verkettete Begrenzungen außerhalb einer Partition erstellen, verwerfen Sie die inneren Begrenzungen, wenn sie nicht mehr verwendet werden.
  • Bevorzugen Sie den globalen PartitionedRateLimiter.CreateChained Ansatz, wenn die Kette auf jeden Endpunkt angewendet werden soll. Verwenden Sie eine benannte Richtlinie mit RateLimiter.CreateChained nur, wenn die Kette auf bestimmte Endpunkte beschränkt werden muss.

Auswählen, was passiert, wenn eine Anfrage rate-limitiert ist

Für einfache Fälle können Sie einfach den Statuscode festlegen:

builder.Services.AddRateLimiter(options =>
{
    // Set a custom status code for rejections
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // Rate limiter configuration...
});

Der gängigste Ansatz besteht darin, beim Konfigurieren der Ratenbegrenzung einen OnRejected Callback zu registrieren:

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

Eine weitere Option besteht darin, die Anforderung in die Warteschlange zu stellen:

Anforderungswarteschlange

Wenn Sie die Warteschlangenfunktion aktivieren und eine Anforderung das Ratenlimit überschreitet, stellt das System sie in eine Warteschlange. Die Anforderung wartet in der Warteschlange, bis eine Genehmigung verfügbar ist oder ein Timeout auftritt. Das System verarbeitet Anforderungen gemäß einer konfigurierbaren Warteschlangenreihenfolge.

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- und DisableRateLimiting-Attribute

Wenden Sie die [EnableRateLimiting] Attribute auf [DisableRateLimiting] einen Controller, eine Aktionsmethode oder Razor eine Seite an. Bei Razor Seiten wenden Sie das Attribut auf die Razor Seite und nicht auf die Seitenhandler an. Sie können OnPost beispielsweise nicht auf [EnableRateLimiting], OnGet oder einen anderen Seitenhandler anwenden.

Das [DisableRateLimiting]-Attribut deaktiviert die Ratenbegrenzung für den Controller, die Aktionsmethode oder die Razor-Seite, unabhängig von angewendeten benannten Ratenbegrenzern oder globalen Begrenzern. 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 Program.cs in app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) 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.

Metriken zur Ratenbegrenzung

Die Middleware zur Begrenzung der Rate bietet integrierte Metriken und Überwachungsfunktionen , die Ihnen helfen, zu verstehen, wie Sich Ratelimits auf die Leistung der App und die Benutzererfahrung auswirken. Eine Liste der Metriken finden Sie unter Microsoft.AspNetCore.RateLimiting.

Testen von Endpunkten mit Ratenbegrenzung

Bevor Sie eine App mit Ratenbegrenzung in der Produktionsumgebung bereitstellen, unterziehen Sie sie einem Lasttest, um die verwendeten Ratenbegrenzer und Optionen zu validieren. Erstellen Sie beispielsweise ein JMeter-Skript mithilfe eines Tools wie BlazeMeter oder Apache JMeter HTTP(S) Test Script Recorder, und laden Sie das Skript auf Azure Load Testing.

Wenn Sie Partitionen mithilfe von Benutzereingaben erstellen, ist Ihre App anfällig für Denial of Service (DoS)-Angriffe. Wenn Sie beispielsweise Partitionen mithilfe von Client-IP-Adressen erstellen, wird Ihre App anfällig für Denial-of-Service-Angriffe, die IP-Quelladressenspoofing verwenden. Weitere Informationen finden Sie unter BCP 38 RFC 2827 Netzwerkeingangsfilterung: Abwehr von Denial-of-Service-Angriffen mit IP-Quelladressen-Spoofing.

Zusätzliche Ressourcen