Snelheidsbeperking middleware in ASP.NET Core
Door Arvin Kahbazi, Maarten Balliauwen Rick Anderson
De Microsoft.AspNetCore.RateLimiting
middleware biedt snelheidsbeperkende middleware. Apps configureren beleidsregels voor snelheidsbeperking en voegen vervolgens het beleid toe aan eindpunten. Apps die frequentielimiet gebruiken, moeten zorgvuldig worden getest en gecontroleerd voordat ze worden geïmplementeerd. Zie Testeindpunten met snelheidsbeperking in dit artikel voor meer informatie.
Zie Rate limiting middlewarevoor een inleiding tot snelheidsbeperking.
Algoritmen voor frequentielimieten
De RateLimiterOptionsExtensions
-klasse biedt de volgende uitbreidingsmethoden voor snelheidsbeperking:
Vaste vensterbegrenzer
De methode AddFixedWindowLimiter
gebruikt een vast tijdvenster om aanvragen te beperken. Wanneer het tijdvenster verloopt, wordt een nieuw tijdvenster gestart en wordt de aanvraaglimiet opnieuw ingesteld.
Houd rekening met de volgende 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();
De voorgaande code:
- Roept AddRateLimiter aan om een snelheidsbeperkingsservice toe te voegen aan de serviceverzameling.
- Roept
AddFixedWindowLimiter
aan om een vaste vensterbegrenzer te maken met een beleidsnaam van"fixed"
en sets: - PermitLimit tot 4 en de tijd Window tot 12. Er zijn maximaal 4 aanvragen per elk venster van 12 seconden toegestaan.
- QueueProcessingOrder tot OldestFirst.
- QueueLimit tot 2.
- Roept UseRateLimiter- aan om snelheidsbeperking in te schakelen.
Apps moeten Configuration gebruiken om opties voor limieten in te stellen. Met de volgende code wordt de voorgaande code bijgewerkt met behulp van MyRateLimitOptions
voor configuratie:
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 moet worden aangeroepen na UseRouting
wanneer eindpuntspecifieke API's voor frequentiebeperking worden gebruikt. Als het kenmerk [EnableRateLimiting]
bijvoorbeeld wordt gebruikt, moet UseRateLimiter
worden aangeroepen na UseRouting
. Wanneer u alleen globale begrenzers aanroept, kan UseRateLimiter
worden aangeroepen voordat UseRouting
.
Schuifvensterbegrenzer
Een algoritme voor een schuifvenster:
- Is vergelijkbaar met de vaste vensterbegrenzer, maar voegt segmenten per venster toe. Het venster schuift één segment per segmentinterval. Het segmentinterval is (tijdvenster)/(segmenten per venster).
- Hiermee beperkt u het aantal aanvragen voor een venster tot
permitLimit
. - Elke tijdvenster wordt verdeeld in
n
segmenten per venster. - Aanvragen uit het verlopen tijdsegment één venster terug (
n
segment vóór het huidige segment) worden toegevoegd aan het huidige segment. We verwijzen naar het meest verlopen tijdsegment één venster terug als het verlopen segment.
Bekijk de volgende tabel met een schuifvensterbegrenzer met een venster van 30 seconden, drie segmenten per venster en een limiet van 100 aanvragen:
- In de bovenste rij en eerste kolom wordt het tijdsegment weergegeven.
- In de tweede rij ziet u de resterende aanvragen die beschikbaar zijn. De resterende aanvragen worden berekend als de beschikbare aanvragen minus de verwerkte aanvragen plus de gerecyclede aanvragen.
- Aanvragen bewegen elke keer langs de diagonale blauwe lijn.
- Vanaf tijd 30 wordt de aanvraag uit het verlopen tijdsegment weer toegevoegd aan de aanvraaglimiet, zoals wordt weergegeven in de rode regels.
In de volgende tabel ziet u de gegevens in de vorige grafiek in een andere indeling. In de kolom Beschikbaar worden de aanvragen weergegeven die beschikbaar zijn in het vorige segment (de uit de vorige rij overdragen). In de eerste rij ziet u 100 beschikbare aanvragen omdat er geen vorig segment is.
Tijd | Beschikbaar | Gepakt | Gerecycled van verlopen materialen | Overdracht |
---|---|---|---|---|
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 |
De volgende code maakt gebruik van de frequentielimiet voor schuifvensters:
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();
Token bucket-begrenzer
De tokenbucketbegrenzer is vergelijkbaar met de schuifvensterbegrenzer, maar in plaats van de aanvragen uit het verlopen segment toe te voegen, wordt elke aanvullingsperiode een vast aantal tokens toegevoegd. De tokens die aan elk segment zijn toegevoegd, kunnen de beschikbare tokens niet verhogen tot een getal dat hoger is dan de limiet voor de tokenbucket. In de volgende tabel ziet u een tokenbucketbegrenzer met een limiet van 100 tokens en een aanvullingsperiode van 10 seconden.
Tijd | Beschikbaar | Genomen | Toegevoegd | Overdracht |
---|---|---|---|---|
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 |
De volgende code maakt gebruik van de bucketbegrenzer voor tokens:
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();
Wanneer AutoReplenishment is ingesteld op true
, vult een interne timer de tokens aan met een interval van ReplenishmentPeriod; als deze is ingesteld op false
, moet de app TryReplenish aanroepen op de limiter.
Limiet voor gelijktijdigheid
De gelijktijdigheidsbeperking beperkt het aantal gelijktijdige aanvragen. Elke aanvraag vermindert de gelijktijdigheidslimiet met één. Wanneer een aanvraag is voltooid, wordt de limiet met één verhoogd. In tegenstelling tot de andere limieten voor aanvragen die het totale aantal aanvragen voor een opgegeven periode beperken, beperkt de limiet voor gelijktijdigheid alleen het aantal gelijktijdige aanvragen en wordt het aantal aanvragen in een bepaalde periode niet beperkt.
De volgende code maakt gebruik van de gelijktijdigheidslimiet:
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();
Gekoppelde begrenzers maken
Met de CreateChained-API kunnen meerdere PartitionedRateLimiter worden doorgegeven die in één PartitionedRateLimiter
worden gecombineerd. Met de gecombineerde begrenzer worden alle invoerbegrenzers op volgorde uitgevoerd.
De volgende code maakt gebruik van 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();
Zie de CreateChained-broncode voor meer informatie
kenmerken EnableRateLimiting
en DisableRateLimiting
De kenmerken [EnableRateLimiting]
en [DisableRateLimiting]
kunnen worden toegepast op een controller, actiemethode of Razor-pagina. Voor Razor Pagina's moet het kenmerk worden toegepast op de Razor Pagina en niet op de pagina-handlers.
[EnableRateLimiting]
kan bijvoorbeeld niet worden toegepast op OnGet
, OnPost
of een andere pagina-handler.
Het kenmerk [DisableRateLimiting]
schakelt snelheidsbeperking voor controller, actiemethode of Razor pagina uit, ongeacht de benoemde frequentielimieten of globale limieten die zijn toegepast. Denk bijvoorbeeld aan de volgende code die RequireRateLimiting aanroept om de fixedPolicy
frequentielimiet toe te passen op alle controllereindpunten:
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();
In de volgende code schakelt [DisableRateLimiting]
snelheidsbeperking uit en negeert de [EnableRateLimiting("fixed")]
die wordt toegepast op de Home2Controller
en app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
die in Program.cs
worden aangeroepen.
[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 });
}
}
In de voorgaande code wordt de [EnableRateLimiting("sliding")]
niet toegepast op de actiemethode Privacy
omdat Program.cs
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
genoemd.
Houd rekening met de volgende code die geen RequireRateLimiting
aanroept op MapRazorPages
of MapDefaultControllerRoute
:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages();
app.MapDefaultControllerRoute(); // RequireRateLimiting not called
app.Run();
Houd rekening met de volgende 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 });
}
}
In de voorgaande controller:
- De
"fixed"
beleidsfrequentielimiet wordt toegepast op alle actiemethoden die geenEnableRateLimiting
enDisableRateLimiting
kenmerken hebben. - De
"sliding"
beleidssnelheidslimiet wordt toegepast op de actiePrivacy
. - Snelheidsbeperking is uitgeschakeld voor de actiemethode
NoLimit
.
Kenmerken toepassen op Razor pagina’s
Voor Razor Pagina's moet het kenmerk worden toegepast op de Razor Pagina en niet op de pagina-handlers.
[EnableRateLimiting]
kan bijvoorbeeld niet worden toegepast op OnGet
, OnPost
of een andere pagina-handler.
Het kenmerk DisableRateLimiting
schakelt snelheidsbeperking op een Razor-pagina uit.
Vergelijking van beperkende algoritmen
De vaste, schuif- en tokenbegrenzers beperken allemaal het maximum aantal aanvragen in een bepaalde periode. De gelijktijdigheidsbeperking beperkt alleen het aantal gelijktijdige aanvragen en beperkt het aantal aanvragen in een bepaalde periode niet. De kosten van een eindpunt moeten worden overwogen bij het selecteren van een begrenzer. De kosten van een eindpunt omvatten de gebruikte resources, bijvoorbeeld tijd, gegevenstoegang, CPU en I/O.
Voorbeelden van frequentielimieten
De volgende voorbeelden zijn niet bedoeld voor productiecode, maar zijn voorbeelden van het gebruik van de limieten.
Begrenzer met OnRejected
, RetryAfter
en GlobalLimiter
Het volgende voorbeeld:
Hiermee maakt u een RateLimiterOptions.OnRejected callback die wordt aangeroepen wanneer een aanvraag de opgegeven limiet overschrijdt.
retryAfter
kan worden gebruikt met de TokenBucketRateLimiter, FixedWindowLimiteren SlidingWindowLimiter omdat deze algoritmen kunnen schatten wanneer er meer vergunningen worden toegevoegd. DeConcurrencyLimiter
heeft geen manier om te berekenen wanneer vergunningen beschikbaar zijn.Voegt de volgende limieten toe:
- Een
SampleRateLimiterPolicy
waarmee deIRateLimiterPolicy<TPartitionKey>
-interface wordt geïmplementeerd. DeSampleRateLimiterPolicy
klasse wordt verderop in dit artikel weergegeven. - Een
SlidingWindowLimiter
:- Met een partitie voor elke geverifieerde gebruiker.
- Eén gedeelde partitie voor alle anonieme gebruikers.
- Een GlobalLimiter die wordt toegepast op alle aanvragen. De globale begrenzer wordt eerst uitgevoerd, gevolgd door de eindpuntspecifieke begrenzer, als er een bestaat. De
GlobalLimiter
maakt een partitie voor elke IPAddress.
- Een
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();
Waarschuwing
Als u partities maakt op client-IP-adressen, is de app kwetsbaar voor Denial of Service-aanvallen die gebruikmaken van IP-bronadresvervalsing. Zie BCP 38 RFC 2827 Network Ingress Filtering: Denial of Service-aanvallen verslaan die gebruikmaken van IP-bronadresvervalsingvoor meer informatie.
Zie de opslagplaats met voorbeelden voor het volledige Program.cs
-bestand.
De SampleRateLimiterPolicy
-klasse
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
});
}
}
In de voorgaande code gebruikt OnRejectedOnRejectedContext om de antwoordstatus in te stellen op 429 Te veel aanvragen. De standaard geweigerde status is 503 Service Unavailable.
Begrenzer met autorisatie
In het volgende voorbeeld wordt JSON-webtokens (JWT) gebruikt en wordt een partitie gemaakt met het JWT-toegangstoken. In een productie-app wordt de JWT doorgaans geleverd door een server die fungeert als een beveiligingstokenservice (STS). Voor lokale ontwikkeling kan het dotnet user-jwts opdrachtregelprogramma worden gebruikt om app-specifieke lokale JWT's te maken en te beheren.
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}";
Begrenzer met ConcurrencyLimiter
, TokenBucketRateLimiter
en autorisatie
Het volgende voorbeeld:
- Hiermee voegt u een
ConcurrencyLimiter
toe met een beleidsnaam van"get"
die wordt gebruikt op de Razor Pagina's. - Voegt een
TokenBucketRateLimiter
toe met een partitie voor elke geautoriseerde gebruiker en een partitie voor alle anonieme gebruikers. - Stelt RateLimiterOptions.RejectionStatusCode in op 429 Te veel verzoeken.
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
});
}));
Zie de opslagplaats met voorbeelden voor het volledige Program.cs
-bestand.
Eindpunten testen met snelheidsbeperking
Voordat u een app implementeert met snelheidsbeperking voor productie, test u de app met stress om de frequentielimieten en gebruikte opties te valideren. Maak bijvoorbeeld een JMeter-script met een hulpprogramma zoals BlazeMeter of Apache JMeter HTTP(S) Test Script Recorder en laad het script naar Azure Load Testing.
Als u partities maakt met gebruikersinvoer, is de app kwetsbaar voor DoS-aanvallen (Denial of Service). Als u bijvoorbeeld partities maakt op client-IP-adressen, is de app kwetsbaar voor Denial of Service-aanvallen die gebruikmaken van IP-bronadresvervalsing. Zie voor meer informatie BCP 38 RFC 2827 Netwerktoegangsfiltering: Het tegengaan van Denial of Service-aanvallen die IP-adresvervalsing gebruiken.
Aanvullende informatiebronnen
- Rate limiting middleware van Maarten Balliauw biedt een uitstekende inleiding en overzicht van snelheidsbeperking.
- frequentielimiet voor een HTTP-handler in .NET