Hastighetsbegränsning av mellanprogram i ASP.NET Core
Av Arvin Kahbazi, Maarten Balliauwoch Rick Anderson
Den Microsoft.AspNetCore.RateLimiting
-mellanprogrammet tillhandahåller mellanprogram för hastighetsbegränsning. Appar konfigurerar principer för hastighetsbegränsning och kopplar sedan principerna till slutpunkter. Appar som använder hastighetsbegränsning bör noggrant läsas in och granskas innan de distribueras. Mer information finns i Testa slutpunkter med hastighetsbegränsning i den här artikeln.
En introduktion till hastighetsbegränsning finns i Hastighetsbegränsning för mellanprogram.
Frekvensbegränsningsalgoritmer
Klassen RateLimiterOptionsExtensions
innehåller följande tilläggsmetoder för hastighetsbegränsning:
Fast fönstergränsare
Metoden AddFixedWindowLimiter
använder ett fast tidsfönster för att begränsa begäranden. När tidsfönstret upphör att gälla startar ett nytt tidsfönster och begärandegränsen återställs.
Överväg följande kod:
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();
Föregående kod:
- Anropar AddRateLimiter för att lägga till en tjänst för hastighetsbegränsning i tjänstsamlingen.
- Anropar
AddFixedWindowLimiter
för att skapa en fast fönsterbegränsare med policy-namn"fixed"
och anger: - PermitLimit till 4 och tiden Window till 12. Högst 4 begäranden per 12 sekunders fönster tillåts.
- QueueProcessingOrder till OldestFirst.
- QueueLimit till 2.
- Anropar UseRateLimiter för att aktivera hastighetsbegränsning.
Appar bör använda Configuration för att ange begränsningsalternativ. Följande kod uppdaterar föregående kod med hjälp av MyRateLimitOptions
för 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 måste anropas efter UseRouting
när hastighetsbegränsning av slutpunktsspecifika API:er används. Om attributet [EnableRateLimiting]
till exempel används måste UseRateLimiter
anropas efter UseRouting
. När du bara anropar globala begränsare kan UseRateLimiter
anropas innan UseRouting
.
Skjutfönsterbegränsare
En algoritm för skjutfönster:
- Liknar den fasta fönsterbegränsaren men lägger till segment per fönster. Fönstret skjuter ett segment varje segmentintervall. Segmentintervallet är (fönstertid)/(segment per fönster).
- Begränsar begäranden för ett fönster till
permitLimit
begäranden. - Varje tidsfönster delas upp i
n
segment per fönster. - Begäranden som tas från det förfallna tidssegmentet ett fönster bakåt (
n
segment före det aktuella segmentet) läggs till i det aktuella segmentet. Vi avser det senaste utgångna tidssegmentet en period tillbaka som det utgångna segmentet.
Tänk på följande tabell som visar en glidande fönsterbegränsare med ett 30-sekundersfönster, tre segment per fönster och en gräns på 100 begäranden:
- Den översta raden och den första kolumnen visar tidssegmentet.
- Den andra raden visar återstående tillgängliga begäranden. Återstående begäranden beräknas som tillgängliga begäranden minus bearbetade begäranden plus de återvunna begärandena.
- Begäranden vid varje tidpunkt flyttas längs den diagonala blå linjen.
- Från och med tidpunkt 30 läggs begäran från det utgångna tidssegmentet tillbaka till begäransgränsen, som visas i de röda linjerna.
I följande tabell visas data i föregående diagram i ett annat format. Kolumnen Tillgänglig visar begäranden som är tillgängliga från föregående segment (Överför från föregående rad). Den första raden visar 100 tillgängliga begäranden eftersom det inte finns något tidigare segment.
Tid | Tillgänglig | Tagit | Återvinns från utgånget material | Föra vidare |
---|---|---|---|---|
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 |
Följande kod använder den glidande fönsterhastighetsbegränsningen:
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-begränsare
Den s.k. "token bucket limiter" liknar den glidande fönsterbegränsaren, men i stället för att lägga till de förfrågningar som tas från den utgångna sektionen, läggs ett fast antal "tokens" till varje påfyllningsperiod. De tokens som lagts till för varje segment kan inte öka antalet tillgängliga tokens till ett tal som är högre än gränsen för token bucket. I följande tabell visas en token bucket limiter med en gräns på 100 token och en 10-sekunders påfyllningsperiod.
Tid | Tillgänglig | Tagit | Lagt till | Föra över |
---|---|---|---|---|
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 |
Följande kod använder token bucket limiter:
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();
När AutoReplenishment är inställt på true
fyller en intern timer på token varje ReplenishmentPeriod. När den är inställd på false
måste appen anropa TryReplenish på begränsaren.
Samtidighetsbegränsning
Samtidighetsbegränsningen begränsar antalet samtidiga begäranden. Varje begäran minskar samtidighetsgränsen med en. När en begäran har slutförts ökas gränsen med en. Till skillnad från de andra begärandebegränsningarna som begränsar det totala antalet begäranden under en angiven period begränsar samtidighetsbegränsningen endast antalet samtidiga begäranden och begränsar inte antalet begäranden under en tidsperiod.
Följande kod använder samtidighetsbegränsningen:
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();
Skapa länkade begränsare
Med CreateChained-API:et kan du skicka in flera PartitionedRateLimiter som kombineras till en PartitionedRateLimiter
. Den kombinerade begränsaren kör alla ingångsbegränsare i följd.
Följande kod använder 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();
Mer information finns i CreateChained-källkoden
EnableRateLimiting
- och DisableRateLimiting
-attribut
Attributen [EnableRateLimiting]
och [DisableRateLimiting]
kan tillämpas på en kontrollant, åtgärdsmetod eller Razor sida. För Razor Pages måste attributet tillämpas på Razor-sidan och inte sidhanterare. Till exempel kan [EnableRateLimiting]
inte tillämpas på OnGet
, OnPost
eller någon annan sidhanterare.
Attributet [DisableRateLimiting]
inaktiverar hastighetsgränser till kontrollern, åtgärdsmetod eller Razor Sidan oavsett vilka namngivna begränsare eller globala begränsare som används. Tänk till exempel på följande kod som anropar RequireRateLimiting för att tillämpa fixedPolicy
hastighetsbegränsning på alla kontrollantslutpunkter:
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();
I följande kod inaktiverar [DisableRateLimiting]
hastighetsbegränsning och åsidosätter [EnableRateLimiting("fixed")]
som tillämpas på Home2Controller
och app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
, anropade i Program.cs
:
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}
public ActionResult Index()
{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
I föregående kod tillämpas [EnableRateLimiting("sliding")]
inte på Privacy
-åtgärdsmetoden eftersom Program.cs
kallas app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
.
Överväg följande kod som inte anropar RequireRateLimiting
på MapRazorPages
eller 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();
Tänk på följande kontrollant:
[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 });
}
}
I den föregående kontrollern:
- Den
"fixed"
policyhastighetsbegränsaren tillämpas på alla åtgärdsmetoder som inte harEnableRateLimiting
- ochDisableRateLimiting
-attribut. - Den
"sliding"
principens hastighetsbegränsning tillämpas på åtgärdenPrivacy
. - Hastighetsbegränsning är inaktiverat på
NoLimit
-åtgärdsmetoden.
Tillämpa attribut på Razor sidor
För Razor Pages måste attributet tillämpas på Razor-sidan och inte sidhanterare. Till exempel kan [EnableRateLimiting]
inte tillämpas på OnGet
, OnPost
eller någon annan sidhanterare.
Attributet DisableRateLimiting
inaktiverar hastighetsbegränsning på en Razor sida.
EnableRateLimiting
tillämpas bara på en Razor sida om MapRazorPages().RequireRateLimiting(Policy)
har inte anropats.
Jämförelse av limiteralgoritmer
De fasta, glidande och tokenbegränsarna begränsar alla det maximala antalet begäranden under en tidsperiod. Samtidighetsbegränsningen begränsar endast antalet samtidiga begäranden och begränsar inte antalet begäranden under en tidsperiod. Kostnaden för en slutpunkt bör beaktas när du väljer en begränsning. Kostnaden för en slutpunkt omfattar de resurser som används, till exempel tid, dataåtkomst, CPU och I/O.
Frekvensbegränsningsexempel
Följande exempel är inte avsedda för produktionskod, men är exempel på hur du använder begränsarna.
Limiter med OnRejected
, RetryAfter
och GlobalLimiter
Följande exempel:
Skapar en RateLimiterOptions.OnRejected callback-funktion som anropas när en begäran överskrider den angivna gränsen.
retryAfter
kan användas med TokenBucketRateLimiter, FixedWindowLimiteroch SlidingWindowLimiter eftersom dessa algoritmer kan uppskatta när fler tillstånd kommer att läggas till.ConcurrencyLimiter
har inget sätt att beräkna när tillstånd kommer att vara tillgängliga.Lägger till följande begränsare:
- En
SampleRateLimiterPolicy
som implementerarIRateLimiterPolicy<TPartitionKey>
-gränssnittet. KlassenSampleRateLimiterPolicy
visas senare i den här artikeln. - En
SlidingWindowLimiter
:- Med en partition för varje autentiserad användare.
- En delad partition för alla anonyma användare.
- Ett GlobalLimiter som tillämpas på alla förfrågningar. Den globala begränsningen körs först, följt av den slutpunktsspecifika begränsningen, om det finns en sådan.
GlobalLimiter
skapar en partition för varje IPAddress.
- En
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();
Varning
Om du skapar partitioner på klientens IP-adresser blir appen sårbar för Denial of Service-attacker som använder förfalskning av IP-källadresser. För mer information, se BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service Attacks som använder IP-källadressförfalskning.
Se exempellagringsplatsen för den fullständiga Program.cs
filen.
Klassen 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
});
}
}
I föregående kod använder OnRejectedOnRejectedContext för att ange svarsstatusen till 429 för många begäranden. Standardstatusen som avvisas är 503-tjänsten är inte tillgänglig.
Begränsare med behörighet
Följande exempel använder JSON-webbtoken (JWT) och skapar en partition med JWT-åtkomsttoken. I en produktionsapp tillhandahålls JWT vanligtvis av en server som fungerar som en säkerhetstokentjänst (STS). För lokal utveckling kan dotnet user-jwts kommandoradsverktyg användas för att skapa och hantera appspecifika lokala JWT:er.
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}";
Begränsare med ConcurrencyLimiter
, TokenBucketRateLimiter
och auktorisering
Följande exempel:
- Lägger till en
ConcurrencyLimiter
med ett principnamn på"get"
som används på Razor-sidorna. - Lägger till en
TokenBucketRateLimiter
med en partition för varje behörig användare och en partition för alla anonyma användare. - Anger RateLimiterOptions.RejectionStatusCode till 429 för många begäranden.
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
});
}));
Se exempellagringsplatsen för den fullständiga Program.cs
filen.
Testa slutpunkter med hastighetsbegränsning
Innan du distribuerar en app med hastighetsbegränsning till produktion ska du stresstesta appen för att verifiera de hastighetsbegränsningar och alternativ som används. Skapa till exempel ett JMeter-skript med ett verktyg som BlazeMeter eller Apache JMeter HTTP(S) Test Script Recorder och ladda skriptet till Azure Load Testing.
Genom att skapa partitioner med användarindata blir appen sårbar för DoS-attacker (Denial of Service). Om du till exempel skapar partitioner på klient-IP-adresser blir appen sårbar för Denial of Service-attacker som använder förfalskning av IP-källadresser. Mer information finns i BCP 38 RFC 2827 Nätverksingångsfiltrering: Att övervinna DoS-attacker som använder IP-källadress-spoofing.
Ytterligare resurser
- Rate limiting middleware by Maarten Balliauw ger en utmärkt introduktion och översikt över hastighetsbegränsning.
- Hastighetsbegränsning för en HTTP-hanterare i .NET
ASP.NET Core