Share via


Middleware de limitação de taxa no ASP.NET Core

Por Arvin Kahbazi, Maarten Balliauw e Rick Anderson

O middleware Microsoft.AspNetCore.RateLimiting fornece middleware de limitação de taxa. Os aplicativos configuram políticas de limitação de taxa e anexam as políticas aos pontos de extremidade. Os aplicativos que usam a limitação de taxa devem ser cuidadosamente testados e revisados antes da implantação. Confira Testando pontos de extremidade com limitação de taxa neste artigo para obter mais informações.

Para obter uma introdução à limitação de taxa, confira Middleware de limitação de taxa.

Algoritmos de limitador de taxa

A classe RateLimiterOptionsExtensions fornece os seguintes métodos de extensão para limitação de taxa:

Limitador de janela fixa

O método AddFixedWindowLimiter usa uma janela de tempo fixa para limitar as solicitações. Quando a janela de tempo expira, uma nova janela de tempo é iniciada e o limite de solicitação é redefinido.

Considere o seguinte código:

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();

O código anterior:

  • Chama AddRateLimiter para adicionar um serviço de limitação de taxa à coleção de serviços.
  • Chama AddFixedWindowLimiter para criar um limitador de janela fixa com um nome de política "fixed" e define:
  • PermitLimit como 4 e o tempo Window como 12. Um máximo de 4 solicitações por cada janela de 12 segundos são permitidas.
  • QueueProcessingOrder para OldestFirst.
  • QueueLimit para 2.
  • Chama UseRateLimiter para habilitar a limitação de taxa.

Os aplicativos devem usar Configuração para definir opções de limitador. O código a seguir atualiza o código anterior usando MyRateLimitOptions para configuração:

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 ser chamado após UseRouting quando as APIs específicas do ponto de extremidade de limitação de taxa são usadas. Por exemplo, se o atributo [EnableRateLimiting] for usado, UseRateLimiter deverá ser chamado após UseRouting. Ao chamar apenas limitadores globais, UseRateLimiter pode ser chamado antes de UseRouting.

Limitador de janela deslizante

Um algoritmo de janela deslizante:

  • É semelhante ao limitador de janela fixa, mas adiciona segmentos por janela. A janela desliza um segmento a cada intervalo de segmento. O intervalo do segmento é (tempo da janela)/(segmentos por janela).
  • Limita as solicitações de uma janela a solicitações permitLimit.
  • Cada janela de tempo é dividida em n segmentos por janela.
  • As solicitações retiradas do segmento de tempo expirado de uma janela (n segmentos anteriores ao segmento atual) são adicionadas ao segmento atual. Nos referimos ao segmento de tempo mais expirado uma janela de volta como o segmento expirado.

Considere a tabela a seguir, que mostra um limitador de janela deslizante com uma janela de 30 segundos, três segmentos por janela e um limite de 100 solicitações:

  • A primeira linha e a primeira coluna mostram o segmento de tempo.
  • A segunda linha mostra as solicitações restantes disponíveis. As solicitações restantes são calculadas como as solicitações disponíveis menos as solicitações processadas mais as solicitações recicladas.
  • As solicitações em cada vez se movem ao longo da linha azul diagonal.
  • A partir do tempo 30 em diante, a solicitação feita do segmento de tempo expirado é adicionada de volta ao limite de solicitação, conforme mostrado nas linhas vermelhas.

Table showing requests, limits, and recycled slots

A tabela a seguir mostra os dados no grafo anterior em um formato diferente. A coluna Disponível mostra as solicitações disponíveis do segmento anterior (O Transporte da linha anterior). A primeira linha mostra 100 disponíveis porque não há nenhum segmento anterior.

Tempo Disponível Obtido Reciclado de expirado Carregar
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

O código a seguir usa o limitador de taxa de janela 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 bucket de token

O limitador de bucket de token é semelhante ao limitador de janela deslizante, mas em vez de adicionar de volta as solicitações feitas do segmento expirado, um número fixo de tokens é adicionado a cada período de reabastecimento. Os tokens adicionados a cada segmento não podem aumentar os tokens disponíveis para um número maior que o limite do bucket de token. A tabela a seguir mostra um limitador de bucket de token com um limite de 100 tokens e um período de reabastecimento de 10 segundos.

Tempo Disponível Obtido Adicionado Carregar
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

O código a seguir usa o limitador de bucket de 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 é definido como true, um temporizador interno reabastece os tokens a cada ReplenishmentPeriod; quando definido como false, o aplicativo deve chamar TryReplenish no limitador.

Limitador de simultaneidade

O limitador de simultaneidade limita o número de solicitações simultâneas. Cada solicitação reduz o limite de simultaneidade em um. Quando uma solicitação é concluída, o limite é aumentado em um. Ao contrário dos outros limitadores de solicitações que limitam o número total de solicitações para um período especificado, o limitador de simultaneidade limita apenas o número de solicitações simultâneas e não limita o número de solicitações em um período de tempo.

O código a seguir usa o limitador de simultaneidade:

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();

Criar limitadores encadeados

A API CreateChained permite passar vários PartitionedRateLimiter que são combinados em um PartitionedRateLimiter. O limitador combinado executa todos os limitadores de entrada em sequência.

O código a seguir 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 obter mais informações, confira o código-fonte CreateChained

Atributos EnableRateLimiting e DisableRateLimiting

Os atributos [EnableRateLimiting] e [DisableRateLimiting] podem ser aplicados a um Controlador, método de ação ou página Razor. Para páginas Razor, o atributo deve ser aplicado à página Razor e não aos manipuladores de página. Por exemplo, [EnableRateLimiting] não pode ser aplicado a OnGet, OnPost ou a qualquer outro manipulador de página.

O atributo [DisableRateLimiting]desabilita a limitação de taxa ao Controlador, ao método de ação ou à página Razor, independentemente dos limitadores de taxa nomeados ou dos limitadores globais aplicados. Por exemplo, considere o seguinte código que chama RequireRateLimiting para aplicar a limitação de taxa fixedPolicy a todos os pontos de extremidade do 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();

No código a seguir, [DisableRateLimiting] desabilita a limitação de taxa e substitui o [EnableRateLimiting("fixed")] aplicado ao Home2Controller e app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) chamados em 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 });
    }
}

No código anterior, o [EnableRateLimiting("sliding")]não é aplicado ao método de ação Privacy porque Program.cs chamou app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy).

Considere o seguinte código que não chama RequireRateLimiting em MapRazorPages ou 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 o seguinte 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 });
    }
}

No controlador anterior:

  • O limitador de taxa de política "fixed" é aplicado a todos os métodos de ação que não têm atributos EnableRateLimiting e DisableRateLimiting.
  • O limitador de taxa de política "sliding" é aplicado à ação Privacy.
  • A limitação de taxa está desabilitada no método de ação NoLimit.

Aplicando atributos a páginas Razor

Para páginas Razor, o atributo deve ser aplicado à página Razor e não aos manipuladores de página. Por exemplo, [EnableRateLimiting] não pode ser aplicado a OnGet, OnPost ou a qualquer outro manipulador de página.

O atributo DisableRateLimiting desabilita a limitação de taxa em uma página Razor. EnableRateLimiting só será aplicado a uma página Razor se MapRazorPages().RequireRateLimiting(Policy)não tiver sido chamado.

Comparação de algoritmos de limitador

Os limitadores fixos, deslizantes e de token limitam o número máximo de solicitações em um período de tempo. O limitador de simultaneidade limita apenas o número de solicitações simultâneas e não limita o número de solicitações em um período de tempo. O custo de um ponto de extremidade deve ser considerado ao selecionar um limitador. O custo de um ponto de extremidade inclui os recursos usados – por exemplo, tempo, acesso a dados, CPU e E/S.

Exemplos de limitador de taxa

Os exemplos a seguir não são destinados ao código de produção, mas são exemplos de como usar os limitadores.

Limitador com OnRejected, RetryAfter e GlobalLimiter

O exemplo a seguir:

  • Cria um retorno de chamada RateLimiterOptions.OnRejected que é chamado quando uma solicitação excede o limite especificado. retryAfter pode ser usado com o TokenBucketRateLimiter, FixedWindowLimiter e SlidingWindowLimiter porque esses algoritmos são capazes de estimar quando mais permissões serão adicionadas. O ConcurrencyLimiter não tem como calcular quando as licenças estarão disponíveis.

  • Adiciona os seguintes limitadores:

    • Um SampleRateLimiterPolicy que implementa a interface IRateLimiterPolicy<TPartitionKey>. A classe SampleRateLimiterPolicy é exibida posteriormente neste tópico.
    • Um SlidingWindowLimiter:
      • Com uma partição para cada usuário autenticado.
      • Uma partição compartilhada para todos os usuários anônimos.
    • Um GlobalLimiter que é aplicado a todas as solicitações. O limitador global será executado primeiro, seguido pelo limitador específico do ponto de extremidade, se houver. O GlobalLimiter cria uma partição 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();

Aviso

A criação de partições em endereços IP do cliente torna o aplicativo vulnerável a ataques de negação de serviço que empregam falsificação de endereço IP de origem. Para obter mais informações, confira Filtragem de entrada de rede DO BCP 38 RFC 2827: derrotar ataques de negação de serviço que empregam falsificação de endereço IP de origem.

Confira o repositório de exemplos para obter o arquivo Program.cs completo.

A 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
            });
    }
}

No código anterior, OnRejected usa OnRejectedContext para definir a resposta status como 429 Excesso de solicitações. O padrão rejeitado status é 503 Serviço indisponível.

Limitador com autorização

O exemplo a seguir usa Tokens Web JSON (JWT) e cria uma partição com o token de acesso JWT. Em um aplicativo de produção, o JWT normalmente seria fornecido por um servidor que atua como um serviço de token de segurança (STS). Para desenvolvimento local, a ferramenta de linha de comando dotnet user-jwts pode ser usada para criar e gerenciar JWTs locais específicos do aplicativo.

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 com ConcurrencyLimiter, TokenBucketRateLimiter e autorização

O exemplo a seguir:

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
            });
    }));

Confira o repositório de exemplos para obter o arquivo Program.cs completo.

Testando pontos de extremidade com limitação de taxa

Antes de implantar um aplicativo à produção usando a limitação de taxa, teste o aplicativo para validar os limitadores de taxa e as opções usadas. Por exemplo, crie um script JMeter com uma ferramenta como o BlazeMeter ou o Gravador de Script de Teste HTTP(S) do Apache JMeter e carregue o script no Teste de Carga do Azure.

A criação de partições com entrada do usuário torna o aplicativo vulnerável a ataques de Negação de Serviço (DoS). Por exemplo, a criação de partições em endereços IP do cliente torna o aplicativo vulnerável a ataques de negação de serviço que empregam falsificação de endereço IP de origem. Para obter mais informações, confira Filtragem de entrada de rede DO BCP 38 RFC 2827: derrotar ataques de negação de serviço que empregam falsificação de endereço IP de origem.

Recursos adicionais