Middleware de limitación de velocidad en ASP.NET Core
Por Arvin Kahbazi, Maarten Balliauw y Rick Anderson
El middleware Microsoft.AspNetCore.RateLimiting
proporciona middleware de limitación de velocidad. Las aplicaciones configuran directivas de limitación de velocidad y, a continuación, colocan las directivas en los puntos de conexión. Las aplicaciones que usan la limitación de velocidad deben probarse y revisarse cuidadosamente antes de la implementación. Vea Prueba de puntos de conexión con limitación de velocidad en este artículo para obtener más información.
Para obtener una introducción a la limitación de velocidad, consulte Middleware de limitación de velocidad.
Algoritmos de limitador de velocidad
La RateLimiterOptionsExtensions
clase proporciona los siguientes métodos de extensión para limitar la velocidad:
Limitador de ventana fijo
El AddFixedWindowLimiter
método usa un período de tiempo fijo para limitar las solicitudes. Cuando expira el período de tiempo, se inicia un nuevo período de tiempo y se restablece el límite de solicitudes.
Observe el código siguiente:
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();
El código anterior:
- Llama AddRateLimiter para agregar un servicio de limitación de velocidad a la colección de servicios.
- Llama
AddFixedWindowLimiter
para crear un limitador de ventana fijo con un nombre de directiva de"fixed"
y conjuntos: - PermitLimit a 4 y el tiempo Window a 12. Se permiten un máximo de 4 solicitudes por cada ventana de 12 segundos.
- De QueueProcessingOrder a OldestFirst.
- QueueLimit a 2.
- Llama a UseRateLimiter para habilitar la limitación de velocidad.
Las aplicaciones deben usar Configuración para establecer las opciones del limitador. El código siguiente actualiza el código anterior mediante MyRateLimitOptions
para la configuración:
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();
Se debe llamar a UseRateLimiter después de UseRouting
cuando se usan las API específicas del punto de conexión de limitación de velocidad. Por ejemplo, si se usa el atributo [EnableRateLimiting]
, se debe llamar a UseRateLimiter
después de UseRouting
. Al llamar solo a los limitadores globales, se puede llamar a UseRateLimiter
antes de UseRouting
.
Limitador de ventana deslizante
Algoritmo de ventana deslizante:
- Es similar al limitador de ventana fijo, pero agrega segmentos por ventana. La ventana desliza un segmento cada intervalo de segmento. El intervalo de segmento es (tiempo de ventana)/(segmentos por ventana).
- Limita las solicitudes de una ventana a
permitLimit
las solicitudes. - Cada período de tiempo se divide en
n
segmentos por ventana. - Las solicitudes tomadas del segmento de tiempo expirado una ventana atrás (
n
segmentos anteriores al segmento actual) se agregan al segmento actual. Hacemos referencia al segmento de tiempo más expirado una ventana como segmento expirado.
Considere la siguiente tabla que muestra un limitador de ventana deslizante con una ventana de 30 segundos, tres segmentos por ventana y un límite de 100 peticiones:
- La fila superior y la primera columna muestran el segmento de tiempo.
- La segunda fila muestra las solicitudes restantes disponibles. Las solicitudes restantes se calculan como las solicitudes disponibles menos las solicitudes procesadas más las solicitudes recicladas.
- Las solicitudes se mueven cada vez a lo largo de la línea azul diagonal.
- A partir del tiempo 30, la solicitud tomada del segmento de tiempo expirado se vuelve a agregar al límite de solicitudes, como se muestra en las líneas rojas.
En la tabla siguiente se muestran los datos del gráfico anterior en un formato diferente. En la columna Restante se muestran las solicitudes disponibles en el segmento anterior (El paso a partir de la fila anterior). La primera fila muestra 100 solicitudes disponibles porque no hay ningún segmento anterior.
Tiempo | Disponible | Tomada | Reciclado a partir de expirado | Transporte por encima |
---|---|---|---|---|
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 |
El código siguiente usa el limitador de velocidad de ventana deslizante:
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();
Limitador de cubos de tokens
El limitador de cubos de tokens es similar al limitador de ventana deslizante, pero en lugar de agregar las solicitudes tomadas del segmento expirado, se agrega un número fijo de tokens cada período de reposición. Los tokens agregados a cada segmento no pueden aumentar los tokens disponibles a un número superior al límite del cubo de tokens. En la tabla siguiente se muestra un limitador de cubos de tokens con un límite de 100 tokens y un período de reposición de 10 segundos.
Tiempo | Disponible | Tomada | Se agregó | Transporte por encima |
---|---|---|---|---|
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 |
El código siguiente usa el limitador de cubos de tokens:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var tokenPolicy = "token";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddTokenBucketLimiter(policyName: tokenPolicy, options =>
{
options.TokenLimit = myOptions.TokenLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
options.ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod);
options.TokensPerPeriod = myOptions.TokensPerPeriod;
options.AutoReplenishment = myOptions.AutoReplenishment;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}"))
.RequireRateLimiting(tokenPolicy);
app.Run();
Cuando AutoReplenishment se establece true
en , un temporizador interno reabastece los tokens cada ReplenishmentPeriod; cuando se establece false
en , la aplicación debe llamar TryReplenish al limitador.
Limitador de simultaneidad
El limitador de simultaneidad limita el número de solicitudes simultáneas. Cada solicitud reduce el límite de simultaneidad en uno. Cuando se completa una solicitud, el límite se incrementa en uno. A diferencia de los demás limitadores de solicitudes que limitan el número total de solicitudes para un período especificado, el limitador de simultaneidad limita solo el número de solicitudes simultáneas y no limita el número de solicitudes en un período de tiempo.
El código siguiente usa el limitador de simultaneidad:
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();
Creación de limitadores encadenados
La CreateChained API permite pasar varias PartitionedRateLimiter que se combinan en una PartitionedRateLimiter
. El limitador combinado ejecuta todos los límites de entrada en secuencia.
El código siguiente 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();
Para más información, vea el código fuente de CreateChained
Atributos EnableRateLimiting
y DisableRateLimiting
Los [EnableRateLimiting]
atributos y [DisableRateLimiting]
se pueden aplicar a un controlador, un método de acción o Razor una página. Para Razor Pages, el atributo debe aplicarse a page Razor y no a los controladores de página. Por ejemplo, [EnableRateLimiting]
no se puede aplicar a OnGet
, OnPost
ni a ningún otro controlador de páginas.
El [DisableRateLimiting]
atributo deshabilita la limitación de velocidad para el controlador, el método de acción o Razor la página, independientemente de los límites de velocidad con nombre o los limitadores globales aplicados. Por ejemplo, considere el código siguiente que llama RequireRateLimiting a para aplicar la limitación de fixedPolicy
velocidad a todos los puntos de conexión del controlador:
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();
En el código siguiente, [DisableRateLimiting]
deshabilita la limitación de velocidad y las invalidaciones [EnableRateLimiting("fixed")]
aplicadas a Home2Controller
y app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
llamadas en 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 });
}
}
En el código anterior, [EnableRateLimiting("sliding")]
no se aplica al método de Privacy
acción porque Program.cs
se llama app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
.
Tenga en cuenta el código siguiente que no llama a RequireRateLimiting
en MapRazorPages
o 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();
Considere el siguiente controlador:
[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 });
}
}
En el controlador anterior:
- El
"fixed"
limitador de velocidad de directiva se aplica a todos los métodos de acción que no tienenEnableRateLimiting
atributos yDisableRateLimiting
. - El
"sliding"
limitador de velocidad de directiva se aplica a laPrivacy
acción. - La limitación de velocidad está deshabilitada en el método de
NoLimit
acción.
Aplicación de atributos a Razor Pages
Para Razor Pages, el atributo debe aplicarse a page Razor y no a los controladores de página. Por ejemplo, [EnableRateLimiting]
no se puede aplicar a OnGet
, OnPost
ni a ningún otro controlador de páginas.
El DisableRateLimiting
atributo deshabilita la limitación de velocidad en una Razor página. EnableRateLimiting
solo se aplica a una Razor página si MapRazorPages().RequireRateLimiting(Policy)
no se ha llamado.
Comparación de algoritmos de limitador
Los limitadores fijos, deslizantes y de token limitan el número máximo de solicitudes en un período de tiempo. El limitador de simultaneidad limita solo el número de solicitudes simultáneas y no limita el número de solicitudes en un período de tiempo. El costo de un punto de conexión debe tenerse en cuenta al seleccionar un limitador. El costo de un punto de conexión incluye los recursos usados, por ejemplo, el tiempo, el acceso a datos, la CPU y la E/S.
Ejemplos de limitador de velocidad
Los ejemplos siguientes no están diseñados para el código de producción, pero son ejemplos sobre cómo usar los limitadores.
Limitador con OnRejected
, RetryAfter
y GlobalLimiter
La siguiente muestra:
Crea una devolución de llamada RateLimiterOptions.OnRejected a la que se llama cuando una solicitud supera el límite especificado.
retryAfter
se puede usar conTokenBucketRateLimiter
,FixedWindowLimiter
ySlidingWindowLimiter
porque estos algoritmos pueden calcular cuándo se agregarán más permisos. NoConcurrencyLimiter
tiene forma de calcular cuándo estarán disponibles los permisos.Agrega los límites siguientes:
SampleRateLimiterPolicy
La clase implementa la interfazIRateLimiterPolicy<TPartitionKey>
. La claseSampleRateLimiterPolicy
se muestra más adelante en este articulo.SlidingWindowLimiter
:- Con una partición para cada usuario autenticado.
- Una partición compartida para todos los usuarios anónimos.
- Que GlobalLimiter se aplica a todas las solicitudes. El limitador global se ejecutará primero, seguido del limitador específico del punto de conexión, si existe uno.
GlobalLimiter
Crea una partición para cada IPAddress.
// 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();
Advertencia
La creación de particiones en direcciones IP de cliente hace que la aplicación sea vulnerable a ataques por denegación de servicio que emplean la suplantación de direcciones IP de origen. Para obtener más información, vea Filtrado de ingreso de red BCP 38 RFC 2827: Derrotar los ataques de denegación de servicio que emplean la suplantación de direcciones IP de origen.
Vea el repositorio de ejemplos para obtener el archivo completo Program.cs
.
La clase 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
});
}
}
En el código anterior, OnRejected usa OnRejectedContext para establecer el estado de respuesta en 429 Demasiadas solicitudes. El estado predeterminado rechazado es 503 Servicio no disponible.
Limitador con autorización
En el ejemplo siguiente se usan tokens web JSON (JWT) y se crea una partición con el token de acceso JWT. En una aplicación de producción, normalmente un servidor que actúa como servicio de token de seguridad (STS) proporciona el JWT. Para el desarrollo local, la herramienta de línea de comandos dotnet user-jwts se puede usar para crear y administrar JWT locales específicos de la aplicación.
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}";
Limitador con ConcurrencyLimiter
autorización , TokenBucketRateLimiter
y
La siguiente muestra:
- Agrega un
ConcurrencyLimiter
objeto con un nombre de directiva de"get"
que se usa en las Razor páginas. - Agrega un
TokenBucketRateLimiter
con una partición para cada usuario autorizado y una partición para todos los usuarios anónimos. - Establece RateLimiterOptions.RejectionStatusCode en 429 Demasiadas solicitudes.
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
});
}));
Vea el repositorio de ejemplos para obtener el archivo completo Program.cs
.
Prueba de puntos de conexión con limitación de velocidad
Antes de implementar una aplicación mediante la limitación de velocidad en producción, pruebe la aplicación para validar los límites de velocidad y las opciones usadas. Por ejemplo, cree un script JMeter con una herramienta como BlazeMeter o Apache JMeter HTTP(S) Test Script Recorder y cargue el script en Azure Load Testing.
La creación de particiones con la entrada del usuario hace que la aplicación sea vulnerable a ataques por Denegación de servicio (DoS). Por ejemplo, la creación de particiones en direcciones IP de cliente hace que la aplicación sea vulnerable a ataques de denegación de servicio que emplean la suplantación de direcciones IP de origen. Para obtener más información, vea Filtrado de ingreso de red BCP 38 RFC 2827: Derrotar los ataques de denegación de servicio que emplean la suplantación de direcciones IP de origen.
Recursos adicionales
- El middleware de limitación de velocidad de Maarten Balliauw proporciona una excelente introducción e información general sobre la limitación de velocidad.
- Límite de velocidad de un controlador HTTP en .NET