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.
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 true
festgelegt 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 AttributeEnableRateLimiting
undDisableRateLimiting
verfügen. - Die Richtlinienratenbegrenzung
"sliding"
wird auf diePrivacy
-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 BegrenzungenTokenBucketRateLimiter
,FixedWindowLimiter
undSlidingWindowLimiter
verwendet werden, weil diese Algorithmen schätzen können, wann weitere Genehmigungen hinzugefügt werden. DerConcurrencyLimiter
hat keine Möglichkeit zu berechnen, wann Genehmigungen verfügbar sein werden.Folgende Begrenzungen werden hinzugefügt:
- Eine
SampleRateLimiterPolicy
, die dieIRateLimiterPolicy<TPartitionKey>
-Schnittstelle implementiert. DieSampleRateLimiterPolicy
-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.
- Eine
// 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
- Middleware mit Bandbreitenbegrenzung von Maarten Balliauw bietet eine ausgezeichnete Einführung und Übersicht über die Rate-Limitierung.
- Ratenbegrenzung eines HTTP-Handlers in .NET
ASP.NET Core