Omezování rychlosti middlewaru v ASP.NET Core
Arvin Kahbazi, Maarten Balliauw a Rick Anderson
Middleware Microsoft.AspNetCore.RateLimiting
poskytuje omezování rychlosti middlewaru. Aplikace konfigurují zásady omezování rychlosti a pak zásady připojují ke koncovým bodům. Aplikace využívající omezování rychlosti by se měly před nasazením pečlivě testovat a kontrolovat. Další informace najdete v části Testování koncových bodů s omezováním rychlosti v tomto článku.
Úvod do omezování rychlosti najdete v tématu Omezování rychlosti middleware.
Algoritmy omezování rychlosti
Třída RateLimiterOptionsExtensions
poskytuje následující rozšiřující metody pro omezování rychlosti:
Oprava omezovače oken
Metoda AddFixedWindowLimiter
používá k omezení požadavků pevný časový interval. Po vypršení časového intervalu se spustí nové časové okno a limit požadavku se resetuje.
Uvažujte následující kód:
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();
Předchozí kód:
- Volání AddRateLimiter , která přidají službu omezování rychlosti do kolekce služeb.
- Volání
AddFixedWindowLimiter
k vytvoření pevného omezovače oken s názvem"fixed"
a sadami zásad: - PermitLimit až 4 a doba Window do 12. Je povoleno maximálně 4 požadavky na každé 12sekundové okno.
- QueueProcessingOrder na OldestFirst.
- QueueLimit do 2.
- Volání UseRateLimiter pro povolení omezování rychlosti.
Aplikace by měly používat konfiguraci k nastavení možností omezovače. Následující kód aktualizuje předchozí kód pomocí MyRateLimitOptions
konfigurace:
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 musí být volána po UseRouting
použití rozhraní API pro omezení rychlosti pro konkrétní koncové body. Pokud je například [EnableRateLimiting]
použit atribut, UseRateLimiter
musí být volána za UseRouting
. Při volání pouze globálních limiterů UseRateLimiter
lze volat před UseRouting
.
Posuvný omezovač oken
Algoritmus posuvného okna:
- Podobá se omezovači pevných oken, ale přidává segmenty na okno. Okno se posune o jeden segment každého intervalu segmentů. Interval segmentu je (čas okna)/(segmenty na okno).
- Omezuje požadavky na okno na
permitLimit
žádosti. - Každé časové okno je rozděleno do
n
segmentů na každé okno. - Do aktuálního segmentu se přidají žádosti odebrané z časového segmentu s vypršenou platností (
n
segmenty před aktuálním segmentem). Jako segment s vypršenou platností se odkazujeme na segment, jehož platnost vypršela.
Podívejte se na následující tabulku, která ukazuje omezovač posuvného okna s 30sekundovým oknem, třemi segmenty na okno a limitem 100 požadavků:
- Horní řádek a první sloupec znázorňují časový segment.
- Druhý řádek zobrazuje zbývající dostupné žádosti. Zbývající požadavky se počítají jako dostupné požadavky minus zpracované požadavky a recyklované žádosti.
- Žádosti se vždy pohybují podél diagonální modré čáry.
- Od 30. okamžiku se žádost převzatá z časového segmentu s vypršenou platností přidá zpět do limitu požadavku, jak je znázorněno na červených řádcích.
Následující tabulka ukazuje data v předchozím grafu v jiném formátu. Sloupec K dispozici zobrazuje požadavky dostupné z předchozího segmentu (Přenos z předchozího řádku). První řádek zobrazuje 100 dostupných požadavků, protože neexistuje žádný předchozí segment.
Čas | dostupný | Zaujatý | Recyklace z konce platnosti | Přenést |
---|---|---|---|---|
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 |
Následující kód používá omezovač rychlosti posuvného okna:
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();
Omezovač kontejnerů tokenů
Limitátor kontejneru tokenů je podobný klouzavému omezovači oken, ale místo přidání žádostí přijatých z segmentu s vypršenou platností se každý interval doplňování přidá pevný počet tokenů. Přidané tokeny nemohou zvýšit dostupné tokeny na číslo vyšší než limit kontejneru tokenů. Následující tabulka ukazuje limit kontejneru tokenů s limitem 100 tokenů a 10sekundovým doplňovacím obdobím.
Čas | dostupný | Zaujatý | Přidáno | Přenést |
---|---|---|---|---|
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 |
Následující kód používá omezovač kontejneru tokenů:
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();
Pokud AutoReplenishment je nastavena hodnota true
, vnitřní časovač doplní tokeny vždy ReplenishmentPeriod; pokud je nastavena false
na , aplikace musí volat TryReplenish limiter.
Omezovač souběžnosti
Limiter souběžnosti omezuje počet souběžných požadavků. Každý požadavek snižuje limit souběžnosti o jeden. Po dokončení žádosti se limit zvýší o jeden. Na rozdíl od ostatních omezovačů požadavků, které omezují celkový počet požadavků na zadané období, limiter souběžnosti omezuje pouze počet souběžných požadavků a nesdílí počet požadavků v časovém období.
Následující kód používá limiter souběžnosti:
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();
Vytváření zřetězených omezovačů
Rozhraní CreateChained API umožňuje předávání více PartitionedRateLimiter , které jsou sloučeny do jednoho PartitionedRateLimiter
. Kombinovaný limiter spouští všechny vstupní limitery v posloupnosti.
Následující kód používá 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();
Další informace najdete ve zdrojovém kódu CreateChained.
EnableRateLimiting
a DisableRateLimiting
atributy
Atributy [EnableRateLimiting]
lze [DisableRateLimiting]
použít u kontroleru, metody akce nebo Razor stránky. U Razor stránek musí být atribut použit na Razor stránku, nikoli na obslužné rutiny stránky. [EnableRateLimiting]
Například nelze použít pro OnGet
, OnPost
ani pro žádnou jinou obslužnou rutinu stránky.
Atribut [DisableRateLimiting]
zakáže omezení rychlosti kontroleru, metody akce nebo Razor stránky bez ohledu na pojmenované omezovače rychlosti nebo globální limitery použité. Představte si například následující kód, který volá RequireRateLimiting , aby se omezení rychlosti použilo fixedPolicy
na všechny koncové body kontroleru:
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();
V následujícím kódu [DisableRateLimiting]
zakáže omezování rychlosti a přepsání [EnableRateLimiting("fixed")]
použité u Home2Controller
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
a volána :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 });
}
}
V předchozím kódu [EnableRateLimiting("sliding")]
se nepoužije na metodu Privacy
akce, protože Program.cs
volána app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
.
Představte si následující kód, který nezavolá RequireRateLimiting
MapRazorPages
nebo 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();
Vezměte v úvahu následující kontroler:
[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 });
}
}
V předchozím kontroleru:
- Omezení
"fixed"
rychlosti zásad se použije u všech metod akcí, které nemajíEnableRateLimiting
aDisableRateLimiting
nemají atributy. - U
"sliding"
akce se použijePrivacy
omezení rychlosti zásad. - Omezení rychlosti je u metody akce zakázané
NoLimit
.
Použití atributů na Razor stránky
U Razor stránek musí být atribut použit na Razor stránku, nikoli na obslužné rutiny stránky. [EnableRateLimiting]
Například nelze použít pro OnGet
, OnPost
ani pro žádnou jinou obslužnou rutinu stránky.
Atribut DisableRateLimiting
zakáže omezování rychlosti na Razor stránce. EnableRateLimiting
je použita pouze na Razor stránku, pokud MapRazorPages().RequireRateLimiting(Policy)
nebyla volána.
Porovnání limiterů algoritmů
Pevné, posuvné a tokenové omezovače omezují maximální počet požadavků v časovém období. Limiter souběžnosti omezuje pouze počet souběžných požadavků a neskončuje počet požadavků v časovém období. Náklady na koncový bod by se měly zvážit při výběru limiteru. Náklady na koncový bod zahrnují použité prostředky, například čas, přístup k datům, procesor a vstupně-výstupní operace.
Vzorky omezovače rychlosti
Následující ukázky nejsou určené pro produkční kód, ale představují příklady použití omezovačů.
Limiter s OnRejected
, RetryAfter
a GlobalLimiter
Následující ukázka:
Vytvoří zpětné volání RateLimiterOptions.OnRejected, které se volá, když požadavek překročí zadaný limit.
retryAfter
lze použít sTokenBucketRateLimiter
,FixedWindowLimiter
aSlidingWindowLimiter
protože tyto algoritmy jsou schopny odhadnout, kdy bude přidáno více povolení. NemáConcurrencyLimiter
žádný způsob výpočtu, kdy bude povolení k dispozici.Přidá následující limitery:
- A
SampleRateLimiterPolicy
, která implementujeIRateLimiterPolicy<TPartitionKey>
rozhraní. TřídaSampleRateLimiterPolicy
se zobrazí dále v tomto článku. - A
SlidingWindowLimiter
:- S oddílem pro každého ověřeného uživatele.
- Jeden sdílený oddíl pro všechny anonymní uživatele.
- A GlobalLimiter , který se použije na všechny požadavky. Globální limiter se spustí jako první, za ním bude následovat limiter specifický pro koncový bod, pokud existuje. Vytvoří
GlobalLimiter
oddíl pro každý IPAddress.
- A
// 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();
Upozorňující
Při vytváření oddílů na IP adresách klienta je aplikace zranitelná vůči útokům na dostupnost služby, které využívají falšování identity zdrojové IP adresy. Další informace najdete v tématu BCP 38 RFC 2827 Filtrování příchozího přenosu dat sítě: Porazit útoky na odepření služby, které využívají falšování identity zdrojové IP adresy.
Kompletní soubor najdete v Program.cs
úložišti ukázek.
Třída 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
});
}
}
V předchozím kódu OnRejected slouží OnRejectedContext k nastavení stavu odpovědi na hodnotu 429 Příliš mnoho požadavků. Výchozí odmítnutý stav je 503 Služba není k dispozici.
Limiter s autorizací
Následující ukázka používá webové tokeny JSON (JWT) a vytvoří oddíl s přístupovým tokenem JWT. V produkční aplikaci by JWT obvykle poskytoval server fungující jako služba tokenů zabezpečení (STS). Pro místní vývoj lze nástroj příkazového řádku dotnet user-jwts použít k vytvoření a správě místních JWT specifických pro aplikaci.
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}";
Limiter s ConcurrencyLimiter
, TokenBucketRateLimiter
a autorizace
Následující ukázka:
ConcurrencyLimiter
Přidá název zásady"get"
, který se používá na Razor stránkách.- Přidá oddíl
TokenBucketRateLimiter
s oddílem pro každého autorizovaného uživatele a oddíl pro všechny anonymní uživatele. - Nastaví RateLimiterOptions.RejectionStatusCode na 429 Příliš mnoho požadavků.
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
});
}));
Kompletní soubor najdete v Program.cs
úložišti ukázek.
Testování koncovýchbodůch
Před nasazením aplikace pomocí omezování rychlosti do produkčního prostředí otestujte aplikaci a ověřte použité omezení rychlosti a možnosti. Vytvořte například skript JMeter pomocí nástroje, jako je BlazeMeter nebo Apache JMeter HTTP(S) Test Script Recorder a načtěte skript do služby Azure Load Testing.
Vytváření oddílů se vstupem uživatele způsobí, že aplikace bude zranitelná vůči útokům DoS (Denial of Service ). Vytváření oddílů na IP adresách klienta například způsobí, že aplikace bude zranitelná vůči útokům na dostupnost služby, které využívají falšování identity zdrojové IP adresy. Další informace najdete v tématu BCP 38 RFC 2827 Filtrování příchozího přenosu dat sítě: Porazit útoky na odepření služeb, které využívají falšování identity zdrojové IP adresy.
Další materiály
- Omezení rychlosti middleware od Maarten Balliauw poskytuje vynikající úvod a přehled omezování rychlosti.
- Omezení rychlosti obslužné rutiny HTTP v .NET