Compartir a través de


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.

 Tabla de solicitudes, límites y franjas horarias recicladas

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 trueen , un temporizador interno reabastece los tokens cada ReplenishmentPeriod; cuando se establece falseen , 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, OnPostni 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 tienen EnableRateLimiting atributos y DisableRateLimiting .
  • El "sliding" limitador de velocidad de directiva se aplica a la Privacy 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, OnPostni 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, RetryAftery 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 con TokenBucketRateLimiter, FixedWindowLimitery SlidingWindowLimiter porque estos algoritmos pueden calcular cuándo se agregarán más permisos. No ConcurrencyLimiter tiene forma de calcular cuándo estarán disponibles los permisos.

  • Agrega los límites siguientes:

    • SampleRateLimiterPolicyLa clase implementa la interfazIRateLimiterPolicy<TPartitionKey>. La clase SampleRateLimiterPolicy 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 ConcurrencyLimiterautorización , TokenBucketRateLimitery

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