Middleware di limite di frequenza in ASP.NET Core
Di Arvin Kahbazi, Maarten Balliauw e Rick Anderson
Il middleware fornisce middleware di limitazione della Microsoft.AspNetCore.RateLimiting
velocità. Le app configurano i criteri di limitazione della frequenza e quindi allegano i criteri agli endpoint. Le app che usano la limitazione della frequenza devono essere testate e esaminate attentamente prima della distribuzione. Per altre informazioni, vedere Test degli endpoint con limitazione della frequenza in questo articolo.
Per un'introduzione alla limitazione della frequenza, vedere Middleware di limitazione della frequenza.
Algoritmi limiter di frequenza
La RateLimiterOptionsExtensions
classe fornisce i metodi di estensione seguenti per la limitazione della frequenza:
Limite di finestra fisso
Il AddFixedWindowLimiter
metodo usa un intervallo di tempo fisso per limitare le richieste. Quando scade l'intervallo di tempo, viene avviato un nuovo intervallo di tempo e viene reimpostato il limite di richieste.
Osservare il codice seguente:
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();
Il codice precedente:
- Chiama AddRateLimiter per aggiungere un servizio di limitazione della frequenza alla raccolta di servizi.
- Chiamate
AddFixedWindowLimiter
per creare un limiter di finestra fisso con un nome di criteri e"fixed"
set: - PermitLimit a 4 e il tempo Window a 12. È consentito un massimo di 4 richieste per ogni finestra di 12 secondi.
- Da QueueProcessingOrder a OldestFirst.
- QueueLimit a 2.
- Chiama UseRateLimiter per abilitare la limitazione della frequenza.
Le app devono usare Configuration per impostare le opzioni limiter. Il codice seguente aggiorna il codice precedente usando MyRateLimitOptions
per la configurazione:
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 deve essere chiamato dopo UseRouting
l'uso delle API specifiche dell'endpoint di limitazione della frequenza. Ad esempio, se viene usato l'attributo [EnableRateLimiting]
, UseRateLimiter
deve essere chiamato dopo UseRouting
. Quando si chiamano solo i limiter globali, UseRateLimiter
è possibile chiamare prima UseRouting
di .
Limite finestra scorrevole
Algoritmo finestra scorrevole:
- È simile al limite di finestra fisso, ma aggiunge segmenti per finestra. La finestra scorre un segmento a ogni intervallo di segmento. L'intervallo di segmento è (intervallo di tempo)/(segmenti per finestra).
- Limita le richieste di una finestra alle
permitLimit
richieste. - Ogni intervallo di tempo è diviso in
n
segmenti per finestra. - Le richieste prelevate dal segmento di tempo scaduto indietro (
n
segmenti precedenti al segmento corrente) vengono aggiunte al segmento corrente. Si fa riferimento al segmento di tempo più scaduto di nuovo come segmento scaduto.
Si consideri la tabella seguente che mostra un limite di finestra scorrevole con una finestra di 30 secondi, tre segmenti per finestra e un limite di 100 richieste:
- La riga superiore e la prima colonna mostrano il segmento di tempo.
- La seconda riga mostra le richieste rimanenti disponibili. Le richieste rimanenti vengono calcolate come richieste disponibili meno le richieste elaborate più le richieste riciclate.
- Le richieste in ogni momento si spostano lungo la linea blu diagonale.
- Dall'ora 30 in poi, la richiesta ottenuta dal segmento di tempo scaduto viene aggiunta nuovamente al limite della richiesta, come illustrato nelle righe rosse.
La tabella seguente mostra i dati nel grafico precedente in un formato diverso. La colonna Available (Disponibile) mostra le richieste disponibili dal segmento precedente (Il trasporto dalla riga precedente). La prima riga mostra 100 richieste disponibili perché non è presente alcun segmento precedente.
Time | Disponibile | Preso | Riciclato da scaduto | Trasportare |
---|---|---|---|---|
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 |
Il codice seguente usa il limite di frequenza delle finestre scorrevoli:
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();
Limite del bucket del token
Il limite del bucket del token è simile al limite della finestra scorrevole, ma invece di aggiungere di nuovo le richieste effettuate dal segmento scaduto, viene aggiunto un numero fisso di token ogni periodo di rifornimento. I token aggiunti a ogni segmento non possono aumentare i token disponibili a un numero superiore al limite del bucket del token. La tabella seguente illustra un limite di bucket di token con un limite di 100 token e un periodo di rifornimento di 10 secondi.
Time | Disponibile | Preso | Aggiunto | Trasportare |
---|---|---|---|---|
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 |
Il codice seguente usa il limite del bucket del 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();
Quando AutoReplenishment è impostato su true
, un timer interno ricostituisce i token ogni ReplenishmentPeriod; se impostato su false
, l'app deve chiamare TryReplenish sul limite.
Limite di concorrenza
Il limite di concorrenza limita il numero di richieste simultanee. Ogni richiesta riduce il limite di concorrenza di uno. Al termine di una richiesta, il limite viene aumentato di uno. A differenza degli altri limiter di richieste che limitano il numero totale di richieste per un periodo specificato, il limite di concorrenza limita solo il numero di richieste simultanee e non limita il numero di richieste in un periodo di tempo.
Il codice seguente usa il limite di concorrenza:
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();
Creare limiti concatenati
L'API CreateChained consente il passaggio di più PartitionedRateLimiter elementi combinati in un unico PartitionedRateLimiter
oggetto . Il limiter combinato esegue tutti i limiter di input in sequenza.
Il codice seguente usa 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();
Per altre informazioni, vedere il codice sorgente CreateChained
Attributi EnableRateLimiting
e DisableRateLimiting
Gli [EnableRateLimiting]
attributi e [DisableRateLimiting]
possono essere applicati a un controller, a un metodo di azione o Razor a una pagina. Per Razor Pages, l'attributo deve essere applicato alla Razor pagina e non ai gestori di pagina. Ad esempio, [EnableRateLimiting]
non può essere applicato a OnGet
, OnPost
o a qualsiasi altro gestore di pagine.
L'attributo [DisableRateLimiting]
disabilita la limitazione della frequenza al controller, al metodo di azione o Razor alla pagina indipendentemente dai limiti di frequenza denominati o dai limiter globali applicati. Si consideri ad esempio il codice seguente che chiama RequireRateLimiting per applicare la limitazione della fixedPolicy
frequenza a tutti gli endpoint controller:
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();
Nel codice [DisableRateLimiting]
seguente disabilita la limitazione della frequenza e gli override [EnableRateLimiting("fixed")]
applicati a Home2Controller
e app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
chiamati in 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 });
}
}
Nel codice precedente, l'oggetto [EnableRateLimiting("sliding")]
non viene applicato al Privacy
metodo di azione perché Program.cs
denominato app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
.
Si consideri il codice seguente che non chiama RequireRateLimiting
o MapRazorPages
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();
Prendere in considerazione il controller seguente:
[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 });
}
}
Nel controller precedente:
- Il
"fixed"
limite di velocità dei criteri viene applicato a tutti i metodi di azione che non dispongonoEnableRateLimiting
di attributi eDisableRateLimiting
. - Il
"sliding"
limite di velocità dei criteri viene applicato all'azionePrivacy
. - La limitazione della frequenza è disabilitata nel
NoLimit
metodo di azione.
Applicazione di attributi alle Razor pagine
Per Razor Pages, l'attributo deve essere applicato alla Razor pagina e non ai gestori di pagina. Ad esempio, [EnableRateLimiting]
non può essere applicato a OnGet
, OnPost
o a qualsiasi altro gestore di pagine.
L'attributo DisableRateLimiting
disabilita la limitazione della frequenza in una Razor pagina. EnableRateLimiting
viene applicato a una Razor pagina solo se MapRazorPages().RequireRateLimiting(Policy)
non è stato chiamato.
Confronto tra algoritmi limiter
I limiti fissi, scorrevoli e token limitano tutto il numero massimo di richieste in un periodo di tempo. Il limite di concorrenza limita solo il numero di richieste simultanee e non limita il numero di richieste in un periodo di tempo. Il costo di un endpoint deve essere considerato quando si seleziona un limiter. Il costo di un endpoint include le risorse usate, ad esempio tempo, accesso ai dati, CPU e I/O.
Esempi di limiter di frequenza
Gli esempi seguenti non sono destinati al codice di produzione, ma sono esempi su come usare i limiti.
Limiter con OnRejected
, RetryAfter
e GlobalLimiter
L'esempio seguente:
Crea un callback RateLimiterOptions.OnRejected chiamato quando una richiesta supera il limite specificato.
retryAfter
può essere usato conTokenBucketRateLimiter
,FixedWindowLimiter
eSlidingWindowLimiter
perché questi algoritmi sono in grado di stimare quando verranno aggiunti altri permessi. NonConcurrencyLimiter
ha modo di calcolare quando saranno disponibili i permessi.Aggiunge i limiti seguenti:
- Oggetto
SampleRateLimiterPolicy
che implementa l'interfacciaIRateLimiterPolicy<TPartitionKey>
. LaSampleRateLimiterPolicy
classe viene illustrata più avanti in questo articolo. - A
SlidingWindowLimiter
:- Con una partizione per ogni utente autenticato.
- Una partizione condivisa per tutti gli utenti anonimi.
- Oggetto GlobalLimiter applicato a tutte le richieste. Il limiter globale verrà eseguito per primo, seguito dal limiter specifico dell'endpoint, se presente. Crea
GlobalLimiter
una partizione per ogni IPAddressoggetto .
- Oggetto
// 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();
Avviso
La creazione di partizioni negli indirizzi IP client rende l'app vulnerabile agli attacchi Denial of Service che usano lo spoofing degli indirizzi IP. Per altre informazioni, vedere BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service Attacks which employ IP Source Address Spoofing .For more information, see BCP 38 RFC 2827 Network Ingress filtering: Defeating Denial of Service Attacks which employ IP Source Address Spoofing.
Per il file completoProgram.cs
, vedere il repository degli esempi.
Classe 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
});
}
}
Nel codice OnRejected precedente, OnRejectedContext usa per impostare lo stato della risposta su 429 Troppe richieste. Lo stato predefinito rifiutato è 503 Servizio non disponibile.
Limiter con autorizzazione
L'esempio seguente usa token JSON Web (JWT) e crea una partizione con il token di accesso JWT. In un'app di produzione, il token JWT viene in genere fornito da un server che funge da servizio token di sicurezza. Per lo sviluppo locale, lo strumento da riga di comando dotnet user-jwts può essere usato per creare e gestire JWT locali specifici dell'app.
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 con ConcurrencyLimiter
, TokenBucketRateLimiter
e autorizzazione
L'esempio seguente:
- Aggiunge un
ConcurrencyLimiter
oggetto con un nome di"get"
criterio utilizzato nelle Razor pagine. - Aggiunge un
TokenBucketRateLimiter
oggetto con una partizione per ogni utente autorizzato e una partizione per tutti gli utenti anonimi. - Imposta RateLimiterOptions.RejectionStatusCode su 429 Troppe richieste.
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
});
}));
Per il file completoProgram.cs
, vedere il repository degli esempi.
Test degli endpoint con limitazione della frequenza
Prima di distribuire un'app usando la limitazione della velocità di produzione, testare lo stress dell'app per convalidare i limiti di velocità e le opzioni usate. Ad esempio, creare uno script JMeter con uno strumento come BlazeMeter o Apache JMeter HTTP(S) Test Script Recorder e caricare lo script in Test di carico di Azure.
La creazione di partizioni con input utente rende l'app vulnerabile agli attacchi Denial of Service (DoS). Ad esempio, la creazione di partizioni negli indirizzi IP client rende l'app vulnerabile agli attacchi Denial of Service che usano lo spoofing degli indirizzi IP di origine. Per altre informazioni, vedere BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service Attacks that employ IP Source Address Spoofing .For more information, see BCP 38 RFC 2827 Network Ingress filtering: Defeating Denial of Service Attacks that employ IP Source Address Spoofing.
Risorse aggiuntive
- Il middleware di limitazione della frequenza di Maarten Balliauw offre un'eccellente introduzione e panoramica per limitare la frequenza.
- Limitare la frequenza di un gestore HTTP in .NET