Ograniczanie szybkości oprogramowania pośredniczącego w programie ASP.NET Core
Przez Arvin Kahbazi, Maarten Balliauw i Rick Anderson
Oprogramowanie Microsoft.AspNetCore.RateLimiting
pośredniczące zapewnia ograniczanie szybkości oprogramowania pośredniczącego. Aplikacje konfigurują zasady ograniczania szybkości, a następnie dołączają zasady do punktów końcowych. Aplikacje korzystające z ograniczania szybkości powinny być dokładnie testowane i sprawdzane przed wdrożeniem. Aby uzyskać więcej informacji, zobacz Testowanie punktów końcowych z ograniczaniem szybkości w tym artykule.
Aby zapoznać się z wprowadzeniem do ograniczania szybkości, zobacz Ograniczanie szybkości oprogramowania pośredniczącego.
Algorytmy ogranicznika szybkości
Klasa RateLimiterOptionsExtensions
udostępnia następujące metody rozszerzenia na potrzeby ograniczania szybkości:
Stały ogranicznik okien
Metoda AddFixedWindowLimiter
używa stałego przedziału czasu w celu ograniczenia żądań. Po wygaśnięciu przedziału czasu zostanie uruchomione nowe okno czasowe i zostanie zresetowany limit żądania.
Spójrzmy na poniższy 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();
Powyższy kod ma następujące działanie:
- Wywołania AddRateLimiter w celu dodania usługi ograniczającej szybkość do kolekcji usług.
- Wywołania
AddFixedWindowLimiter
w celu utworzenia stałego limitatora okien z nazwą zasad i zestawami"fixed"
: - PermitLimit do 4 i czas Window do 12. Dozwolone jest maksymalnie 4 żądania na każde 12-sekundowe okno.
- QueueProcessingOrder do OldestFirst.
- QueueLimit do 2.
- Wywołuje metodę UseRateLimiter , aby włączyć ograniczanie szybkości.
Aplikacje powinny używać konfiguracji do ustawiania opcji ogranicznika. Poniższy kod aktualizuje powyższy kod przy użyciu polecenia MyRateLimitOptions
na potrzeby konfiguracji:
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 należy wywołać po UseRouting
użyciu interfejsów API specyficznych dla punktu końcowego ograniczania szybkości. Jeśli na przykład jest używany atrybut, UseRateLimiter
należy wywołać metodę [EnableRateLimiting]
po UseRouting
. Podczas wywoływania tylko globalnych ograniczników UseRateLimiter
można wywołać metodę przed UseRouting
.
Ogranicznik okna przesuwanego
Algorytm okna przesuwanego:
- Jest podobny do stałego limitatora okien, ale dodaje segmenty na okno. Okno przesuwa jeden segment każdego interwału segmentu. Interwał segmentu to (czas okna)/(segmenty na okno).
- Ogranicza żądania dotyczące okna do
permitLimit
żądań. - Każde okno czasowe jest podzielone w
n
segmentach na okno. - Żądania pobrane z wygasłego segmentu czasu z powrotem (
n
segmenty przed bieżącym segmentem) są dodawane do bieżącego segmentu. Odwołujemy się do najbardziej wygasłego segmentu czasu z powrotem jako wygasłego segmentu.
Rozważmy poniższą tabelę, która przedstawia przesuwany ogranicznik okna z 30-sekundowym oknem, trzema segmentami na okno i limitem 100 żądań:
- Górny wiersz i pierwsza kolumna zawierają segment czasu.
- Drugi wiersz zawiera pozostałe dostępne żądania. Pozostałe żądania są obliczane jako dostępne żądania pomniejszone o przetworzone żądania oraz żądania z recyklingu.
- Żądania w każdym momencie są przesuwane wzdłuż ukośnej niebieskiej linii.
- Od czasu 30 na żądanie pobrane z wygasłego segmentu czasu jest dodawane z powrotem do limitu żądań, jak pokazano w czerwonych wierszach.
W poniższej tabeli przedstawiono dane w poprzednim grafie w innym formacie. W kolumnie Dostępne są wyświetlane żądania dostępne z poprzedniego segmentu ( Przenoszenie z poprzedniego wiersza). Pierwszy wiersz zawiera 100 dostępnych żądań, ponieważ nie ma poprzedniego segmentu.
Czas | Dostępny | Podjęte | Odzyskany z wygasłego | Przewoż |
---|---|---|---|---|
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 |
W poniższym kodzie jest używany ogranicznik szybkości okien przesuwnych:
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();
Ogranicznik zasobnika tokenu
Limiter zasobników tokenu jest podobny do limitatora okien przesuwanych, ale zamiast dodawania z powrotem żądań pobranych z wygasłego segmentu, jest dodawana stała liczba tokenów każdego okresu uzupełniania. Tokeny dodane przez poszczególne segmenty nie mogą zwiększyć dostępnych tokenów do liczby wyższej niż limit zasobnika tokenu. W poniższej tabeli przedstawiono limit zasobnika tokenu z limitem 100 tokenów i 10-sekundowym okresem uzupełniania.
Czas | Dostępny | Podjęte | Dodane | Przewoż |
---|---|---|---|---|
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 |
Poniższy kod używa ogranicznika zasobnika tokenu:
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();
Gdy AutoReplenishment jest ustawiona wartość true
, wewnętrzny czasomierz uzupełnia tokeny co ReplenishmentPeriod; po ustawieniu false
na wartość aplikacja musi wywołać TryReplenish limitator.
Ogranicznik współbieżności
Ogranicznik współbieżności ogranicza liczbę współbieżnych żądań. Każde żądanie zmniejsza limit współbieżności o jeden. Po zakończeniu żądania limit zostanie zwiększony o jeden. W przeciwieństwie do innych ograniczników żądań, które ograniczają łączną liczbę żądań dla określonego okresu, limitator współbieżności ogranicza tylko liczbę współbieżnych żądań i nie ogranicza liczby żądań w danym okresie.
Poniższy kod używa ogranicznika współbieżności:
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();
Tworzenie ograniczników łańcuchowych
Interfejs CreateChained API umożliwia przekazywanie wielu PartitionedRateLimiter , które są łączone w jeden PartitionedRateLimiter
element . Połączony ogranicznik uruchamia wszystkie limitery wejściowe w sekwencji.
Poniższy kod używa metody 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();
Aby uzyskać więcej informacji, zobacz kod źródłowy CreateChained
EnableRateLimiting
i DisableRateLimiting
atrybuty
Atrybuty [EnableRateLimiting]
i [DisableRateLimiting]
można zastosować do kontrolera, metody akcji lub Razor strony. W przypadku Razor stron atrybut musi być stosowany do strony Razor , a nie do programów obsługi stron. Na przykład [EnableRateLimiting]
nie można zastosować do OnGet
programu , OnPost
ani żadnego innego programu obsługi stron.
Atrybut [DisableRateLimiting]
wyłącza ograniczanie szybkości kontrolera, metody akcji lub Razor strony niezależnie od nazwanych ograniczników szybkości lub globalnych ograniczników. Rozważmy na przykład następujący kod, który wywołuje RequireRateLimiting metodę fixedPolicy
stosowania ograniczania szybkości do wszystkich punktów końcowych kontrolera:
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();
W poniższym kodzie [DisableRateLimiting]
wyłącza ograniczanie szybkości i przesłonięcia [EnableRateLimiting("fixed")]
stosowane do elementu i app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
wywoływane Home2Controller
w Program.cs
pliku :
[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 });
}
}
W poprzednim kodzie [EnableRateLimiting("sliding")]
element nie jest stosowany do Privacy
metody akcji, ponieważ Program.cs
o nazwie app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
.
Rozważ następujący kod, który nie wywołuje RequireRateLimiting
MapRazorPages
metody lub 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();
Rozważmy następujący 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 });
}
}
W poprzednim kontrolerze:
- Ogranicznik
"fixed"
szybkości zasad jest stosowany do wszystkich metod akcji, które nie mająEnableRateLimiting
atrybutów iDisableRateLimiting
. - Limiter
"sliding"
szybkości zasad jest stosowany doPrivacy
akcji. - Ograniczanie szybkości jest wyłączone w metodzie
NoLimit
akcji.
Stosowanie atrybutów do Razor stron
W przypadku Razor stron atrybut musi być stosowany do strony Razor , a nie do programów obsługi stron. Na przykład [EnableRateLimiting]
nie można zastosować do OnGet
programu , OnPost
ani żadnego innego programu obsługi stron.
Atrybut DisableRateLimiting
wyłącza ograniczanie szybkości na Razor stronie. EnableRateLimiting
element jest stosowany tylko do Razor strony, jeśli MapRazorPages().RequireRateLimiting(Policy)
nie został wywołany.
Porównanie algorytmów ogranicznika
Stałe, przesuwane i limitatory tokenów ograniczają maksymalną liczbę żądań w danym okresie. Ogranicznik współbieżności ogranicza tylko liczbę współbieżnych żądań i nie ogranicza liczby żądań w danym okresie. Koszt punktu końcowego należy wziąć pod uwagę podczas wybierania ogranicznika. Koszt punktu końcowego obejmuje używane zasoby, na przykład czas, dostęp do danych, procesor CPU i operacje we/wy.
Przykłady ogranicznika szybkości
Poniższe przykłady nie są przeznaczone dla kodu produkcyjnego, ale są przykładami dotyczącymi używania ograniczników.
Ogranicznik z , OnRejected
RetryAfter
iGlobalLimiter
Poniższy przykład:
Tworzy wywołanie zwrotne RateLimiterOptions.OnRejected , które jest wywoływane, gdy żądanie przekracza określony limit.
retryAfter
można używać z elementamiTokenBucketRateLimiter
,FixedWindowLimiter
iSlidingWindowLimiter
, ponieważ te algorytmy są w stanie oszacować, kiedy zostaną dodane więcej zezwoleń. NieConcurrencyLimiter
ma możliwości obliczenia, kiedy zezwolenia będą dostępne.Dodaje następujące ograniczniki:
- Element
SampleRateLimiterPolicy
, który implementujeIRateLimiterPolicy<TPartitionKey>
interfejs. Klasa jest wyświetlanaSampleRateLimiterPolicy
w dalszej części tego artykułu. - A
SlidingWindowLimiter
:- Z partycją dla każdego uwierzytelnionego użytkownika.
- Jedna udostępniona partycja dla wszystkich użytkowników anonimowych.
- Wartość GlobalLimiter , która jest stosowana do wszystkich żądań. Globalny limiter zostanie wykonany najpierw, a następnie ogranicznik specyficzny dla punktu końcowego, jeśli istnieje. Obiekt
GlobalLimiter
tworzy partycję dla każdego IPAddresselementu .
- Element
// 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();
Ostrzeżenie
Tworzenie partycji na adresach IP klienta sprawia, że aplikacja jest podatna na ataki typu "odmowa usługi", które korzystają z fałszowania adresów źródłowych IP. Aby uzyskać więcej informacji, zobacz Filtrowanie ruchu przychodzącego sieci BCP 38 RFC 2827: pokonanie ataków typu "odmowa usługi", które wykorzystują fałszowanie adresów źródłowych IP.
Zobacz repozytorium przykładów, aby uzyskać pełny Program.cs
plik.
Klasa 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
});
}
}
W poprzednim kodzie użyto metody , OnRejected aby ustawić stan odpowiedzi na 429 Zbyt wiele żądań.OnRejectedContext Domyślny odrzucony stan to 503 Usługa niedostępna.
Ogranicznik z autoryzacją
W poniższym przykładzie użyto tokenów sieci Web JSON (JWT) i utworzono partycję przy użyciu tokenu dostępu JWT. W aplikacji produkcyjnej zestaw JWT zazwyczaj jest dostarczany przez serwer działający jako usługa tokenu zabezpieczającego (STS). W przypadku programowania lokalnego narzędzie wiersza polecenia dotnet user-jwts może służyć do tworzenia lokalnych zestawów JWTs specyficznych dla aplikacji i zarządzania nimi.
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}";
Ogranicznik z autoryzacją ConcurrencyLimiter
, TokenBucketRateLimiter
i
Poniższy przykład:
- Dodaje element
ConcurrencyLimiter
z nazwą"get"
zasad, która jest używana na Razor stronach. - Dodaje partycję
TokenBucketRateLimiter
z partycją dla każdego autoryzowanego użytkownika i partycji dla wszystkich użytkowników anonimowych. - Ustawia parametr RateLimiterOptions.RejectionStatusCode na 429 zbyt wiele żądań.
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
});
}));
Zobacz repozytorium przykładów, aby uzyskać pełny Program.cs
plik.
Testowanie punktów końcowych z ograniczaniem szybkości
Przed wdrożeniem aplikacji przy użyciu ograniczania szybkości w środowisku produkcyjnym przetestuj aplikację w celu zweryfikowania używanych ograniczników szybkości i opcji. Na przykład utwórz skrypt JMeter za pomocą narzędzia takiego jak BlazeMeter lub Apache JMeter HTTP(S) Test Script Recorder i załaduj skrypt do testowania obciążenia platformy Azure.
Tworzenie partycji z danymi wejściowymi użytkownika sprawia, że aplikacja jest podatna na ataki typu "odmowa usługi " (DoS). Na przykład tworzenie partycji na adresach IP klienta sprawia, że aplikacja jest podatna na ataki typu "odmowa usługi", które korzystają z fałszowania adresów źródłowych IP. Aby uzyskać więcej informacji, zobacz Filtrowanie ruchu przychodzącego sieci BCP 38 RFC 2827: pokonanie ataków typu "odmowa usługi", które korzystają z fałszowania adresów źródłowych IP.
Dodatkowe zasoby
- Ograniczanie szybkości oprogramowania pośredniczącego przez Maarten Balliauw zapewnia doskonałe wprowadzenie i omówienie ograniczania szybkości.
- Ograniczanie szybkości programu obsługi HTTP na platformie .NET