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 de transferência e anexam as políticas aos endpoints. 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.

Por que usar limitação de taxa

A limitação de taxa pode ser usada para gerenciar o fluxo de solicitações de entrada para um aplicativo. Principais motivos para implementar a limitação de taxa:

  • Prevenção de abuso: a limitação de taxa ajuda a proteger um aplicativo contra abusos limitando o número de solicitações que um usuário ou cliente pode fazer em um determinado período de tempo. Essa proteção é particularmente importante para APIs públicas.
  • Garantindo o uso justo: ao definir limites que impedem os usuários de monopolizar o sistema, você garante que todos os usuários tenham acesso justo aos recursos.
  • Proteção de recursos: a limitação de taxa ajuda a evitar a sobrecarga do servidor controlando o número de solicitações que podem ser processadas. Ele protege os recursos de back-end de serem sobrecarregados.
  • Aumento da segurança: Isso pode mitigar o risco de ataques de negação de serviço (DoS), limitando a taxa com que as solicitações são processadas. Torna mais difícil para os invasores inundar um sistema.
  • Melhorando o desempenho: controlando a taxa de solicitações de entrada, você pode manter o desempenho ideal e a capacidade de resposta de um aplicativo, garantindo uma melhor experiência do usuário.
  • Gerenciamento de Custos: para serviços que incorrem em custos com base no uso, a limitação de taxa pode ajudar a gerenciar e prever despesas controlando o volume de solicitações processadas.

Implementar a limitação de taxa em um aplicativo ASP.NET Core pode ajudar a manter a estabilidade, a segurança e o desempenho. O resultado é um serviço confiável e eficiente para todos os usuários.

Impedir ataques de DDoS

Embora a limitação de taxa possa ajudar a reduzir o risco de ataques de DoS (Negação de Serviço) limitando a taxa na qual as solicitações são processadas, não é uma solução abrangente para ataques de DDoS (Negação de Serviço Distribuído). Os ataques de DDoS envolvem vários sistemas sobrecarregando um aplicativo com uma enxurrada de solicitações, dificultando o processamento apenas com limitação de taxa.

Para uma proteção DDoS robusta, considere o uso de um serviço de proteção contra DDoS comercial. Esses serviços oferecem recursos avançados, como:

  • Análise de tráfego: monitoramento contínuo e análise do tráfego de entrada para detectar e atenuar ataques de DDoS em tempo real.
  • Escalabilidade: a capacidade de lidar com ataques em larga escala distribuindo tráfego entre vários servidores e data centers.
  • Mitigação automatizada: mecanismos de resposta automatizados para bloquear rapidamente o tráfego mal-intencionado sem intervenção manual.
  • Rede global: uma rede global de servidores para absorver e mitigar ataques mais próximos da origem.
  • Atualizações constantes: os serviços comerciais rastreiam e atualizam continuamente seus mecanismos de proteção para se adaptarem a ameaças novas e em evolução.

Ao usar um serviço de hospedagem na nuvem, a proteção contra DDoS geralmente está disponível como parte da solução de hospedagem, como o Firewall de Aplicativo Web do Azure, o AWS Shield ou o Google Cloud Armor. As proteções dedicadas estão disponíveis como WAF (Firewalls de Aplicativo Web) ou como parte de uma solução cdn, como Cloudflare ou Akamai Kona Site Defender

Implementar um serviço de proteção contra DDoS comercial em conjunto com a limitação de taxa pode fornecer uma estratégia de defesa abrangente, garantindo a estabilidade, a segurança e o desempenho de um aplicativo.

Usar middleware de limitação de taxa

As etapas a seguir mostram como usar o middleware de limitação de taxa em um aplicativo ASP.NET Core:

  1. Configurar serviços de limitação de taxa.

No arquivo Program.cs, configure os serviços de limitação de taxa adicionando as políticas apropriadas de limitação de taxa. Defina as políticas como globais ou nomeadas. O exemplo a seguir permite 10 solicitações por minuto por usuário (identidade) ou globalmente:

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 10,
                QueueLimit = 0,
                Window = TimeSpan.FromMinutes(1)
            }));
});

As políticas nomeadas precisam ser aplicadas explicitamente às páginas ou endpoints. O exemplo a seguir adiciona uma política de limitador de janela fixa chamada "fixed", que você adicionará a um endpoint posteriormente:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", opt =>
    {
        opt.PermitLimit = 4;
        opt.Window = TimeSpan.FromSeconds(12);
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 2;
    });
});

var app = builder.Build();

O limitador global é aplicado automaticamente a todos os endpoints quando você o configura via options.GlobalLimiter.

  1. Habilitar middleware de limitação de taxa

    Program.cs No arquivo, habilite o middleware de limitação de taxa chamando UseRateLimiter:

app.UseRouting();

app.UseRateLimiter();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

app.Run();

Aplicar políticas de limitação de taxa a pontos de extremidade ou páginas

Aplicar limitação de taxa de requisições aos endpoints da API Web

Aplique uma política nomeada ao ponto de extremidade ou grupo, por exemplo:


app.MapGet("/api/resource", () => "This endpoint is rate limited")
   .RequireRateLimiting("fixed"); // Apply specific policy to an endpoint

Aplicar limitação de taxa a controladores MVC

Aplique as políticas de limitação de taxa configuradas a pontos de extremidade específicos ou globalmente. Por exemplo, para aplicar a política "fixa" a todos os pontos de extremidade do controlador:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers().RequireRateLimiting("fixed");
});

Aplicar limitação de taxa aos aplicativos Blazor no lado do servidor

Para definir a limitação de taxa para todos os componentes roteáveis Razor do aplicativo, especifique RequireRateLimiting com o nome da política de limitação de taxa na chamada MapRazorComponents no arquivo Program. No exemplo a seguir, a política de limitação de taxa chamada "policy" é aplicada:

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .RequireRateLimiting("policy");

Para definir uma política para um único componente roteável Razor ou uma pasta de componentes por meio de um arquivo de importação (_Imports.razor), aplique o [EnableRateLimiting] atributo com o nome da política. No exemplo a seguir, a política de limitação de taxa chamada "override" é aplicada. A política substituirá qualquer política atualmente aplicada ao endpoint. O limitador global ainda é executado no endpoint com esse atributo aplicado.

@page "/counter"
@using Microsoft.AspNetCore.RateLimiting
@attribute [EnableRateLimiting("override")]

<h1>Counter</h1>

Aplique o atributo [EnableRateLimiting] apenas a um componente roteável ou a uma pasta de componentes usando um arquivo de importações se RequireRateLimitingnão for chamado em MapRazorComponents.

Use o [DisableRateLimiting] atributo para desabilitar a limitação de taxa para um componente roteável ou uma pasta de componentes por meio de um arquivo de importações.

Algoritmos de limitador de taxa

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

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. Considere o custo de um ponto de extremidade ao selecionar um limitador. O custo de um endpoint inclui os recursos usados, como tempo, acesso a dados, CPU e E/S.

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 às OldestFirst.
  • QueueLimit para 2 (defina como 0 para desabilitar o mecanismo de fila).
  • 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, as solicitações retiradas do segmento de tempo expirado são adicionadas novamente ao limite de solicitações, conforme mostrado nas linhas vermelhas.

Tabela mostrando solicitações, limites e slots reciclados

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 solicitações disponíveis porque não há nenhum segmento anterior.

Hora 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.

Hora 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 concorrência

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

Partições de limitação de taxa

As partições de limitação de taxa dividem o tráfego em grupos separados, cada um com seus próprios contadores de limite de taxa. Essa abordagem fornece um controle mais granular do que um único contador global. Chaves diferentes, como ID de usuário, endereço IP ou chave de API, definem os buckets de partição.

Benefícios do particionamento

  • Imparcialidade: um usuário não pode consumir todo o limite de taxa para todos.
  • Granularidade: limites diferentes para diferentes usuários e recursos.
  • Segurança: melhor proteção contra abusos direcionados.
  • Serviço em camadas: suporte para camadas de serviço com limites diferentes.

A limitação de taxa particionada oferece controle refinado sobre como você gerencia o tráfego de API, garantindo uma alocação justa de recursos.

Por endereço IP

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 50,
            Window = TimeSpan.FromMinutes(1)
        }));

Por identidade do usuário

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.User.Identity?.Name ?? "anonymous",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100,
            Window = TimeSpan.FromMinutes(1)
        }));

Por chave de API

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
    string apiKey = httpContext.Request.Headers["X-API-Key"].ToString() ?? "no-key";

    // Different limits based on key tier
    return apiKey switch
    {
        "premium-key" => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: apiKey,
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 1000,
                Window = TimeSpan.FromMinutes(1)
            }),

        _ => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: apiKey,
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1)
            }),
    };
});

Por caminho do ponto de extremidade

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
    string path = httpContext.Request.Path.ToString();

    // Different limits for different paths
    if (path.StartsWith("/api/public"))
    {
        return RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: $"{httpContext.Connection.RemoteIpAddress}-public",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 30,
                Window = TimeSpan.FromSeconds(10)
            });
    }

    return RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100,
            Window = TimeSpan.FromMinutes(1)
        });
});

Criar limitadores encadeados

A CreateChained API aceita várias PartitionedRateLimiter instâncias e as combina em uma PartitionedRateLimiter. O limitador combinado executa todos os limitadores de entrada em sequência. Como o resultado é um PartitionedRateLimiter atribuído a GlobalLimiter, a cadeia se aplica a cada endpoint. Em vez disso, para encadear limitadores para uma extremidade específica, use uma política nomeada, conforme mostrado em Encadear limitadores em uma política nomeada.

O código a seguir usa CreateChained:

using System.Globalization;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(_ =>
{
    _.OnRejected = async (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;
        await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", cancellationToken);
    };
    _.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, consulte o código-fonte CreateChained.

Limitadores de cadeia em uma política nomeada

CreateChained cadeias de limitadores globais que se aplicam a cada ponto de extremidade. Para combinar vários tipos de limitadores e aplicá-los a endpoints específicos, encadeie os limitadores dentro de uma política nomeada com CreateChained. Essa sobrecarga retorna um único RateLimiter que executa cada limitador em sequência, sendo esse o tipo de retorno exigido pela função de criação de partição de uma política nomeada.

A política a seguir "combined" encadeia um limitador de bucket de tokens e um limitador de concorrência com RateLimiter.CreateChained e, em seguida, aplica a política a um único endpoint com RequireRateLimiting:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("combined", httpContext =>
    {
        // Partition on the authenticated identity name when available. Each distinct key creates and
        // caches its own limiter, so partitioning on unbounded user-controlled
        // input can exhaust memory (a DoS risk).
        string partitionKey = httpContext.User.Identity?.Name ?? "anonymous";

        return RateLimitPartition.Get(partitionKey, _ =>
            RateLimiter.CreateChained(
                new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 100,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 5,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(10),
                    TokensPerPeriod = 10,
                    AutoReplenishment = true
                }),
                new ConcurrencyLimiter(new ConcurrencyLimiterOptions
                {
                    PermitLimit = 5,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 2
                })));
    });
});

var app = builder.Build();

app.MapGet("/api/resource", () => "This endpoint uses multiple limiters")
   .RequireRateLimiting("combined");

Uma solicitação deve adquirir uma permissão de cada limitador na cadeia para prosseguir, e os limitadores são processados na ordem passada para RateLimiter.CreateChained. Se um limitador rejeitar a solicitação, a solicitação será rejeitada e as concessões já adquiridas de limitadores anteriores na cadeia serão descartadas em ordem inversa.

Ao combinar limitadores em uma política nomeada, tenha em mente o seguinte:

  • Descartar um arrendamento retorna a permissão para um limitador de concorrência. Os limitadores baseados em tempo (bucket de token, janela fixa e janela deslizante) não retornam uma permissão que já foi adquirida quando um limitador posterior na cadeia rejeita a solicitação, portanto, leve isso em conta ao ordenar os limitadores na cadeia.
  • Construir um TokenBucketRateLimiter diretamente com AutoReplenishment definido como true faz com que cada instância do limitador tenha seu próprio temporizador. As funções auxiliares AddTokenBucketLimiter e RateLimitPartition.GetTokenBucketLimiter, em vez disso, definem AutoReplenishment como false e reabastecem todos os seus limitadores a partir de um único temporizador compartilhado.
  • RateLimiter.CreateChained não libera os limitadores passados a ele. No exemplo anterior, a partição armazena em cache o limitador encadeado e a estrutura gerencia seu tempo de vida. Se você criar limitadores encadeados fora de uma partição, descarte os limitadores internos quando eles não estiverem mais em uso.
  • Prefira a abordagem global PartitionedRateLimiter.CreateChained quando a cadeia deve se aplicar a cada ponto de extremidade. Use uma política nomeada com RateLimiter.CreateChained apenas quando a cadeia precisar ser restrita a pontos de extremidade específicos.

Escolha do que acontece quando uma solicitação é limitada por taxa

Para casos simples, basta definir o código de status:

builder.Services.AddRateLimiter(options =>
{
    // Set a custom status code for rejections
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // Rate limiter configuration...
});

A abordagem mais comum é registrar um OnRejected callback ao configurar a limitação de taxa:

builder.Services.AddRateLimiter(options =>
{
    // Rate limiter configuration...

    options.OnRejected = async (context, cancellationToken) =>
    {
        // Custom rejection handling logic
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.HttpContext.Response.Headers["Retry-After"] = "60";

        await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Please try again later.", cancellationToken);

        // Optional logging
        logger.LogWarning("Rate limit exceeded for IP: {IpAddress}",
            context.HttpContext.Connection.RemoteIpAddress);
    };
});

Outra opção é enfileirar a solicitação:

Enfileiramento de solicitações

Quando você ativa o enfileiramento, se uma solicitação exceder o limite de taxa, o sistema a coloca em uma fila. A solicitação aguarda na fila até que uma licença fique disponível ou ocorra um tempo limite. O sistema processa solicitações de acordo com uma ordem de fila configurável.

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", options =>
    {
        options.PermitLimit = 10;           // Allow 10 requests
        options.Window = TimeSpan.FromSeconds(10);  // Per 10-second window
        options.QueueLimit = 5;             // Queue up to 5 additional requests
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; // Process oldest requests first
        options.AutoReplenishment = true; // Default: automatically replenish permits
    });
});

Atributos EnableRateLimiting e DisableRateLimiting

Aplique os atributos [EnableRateLimiting] e [DisableRateLimiting] a um controlador, método de ação ou página Razor. Para Razor Pages, aplique o atributo à Razor Page e não aos manipuladores da página. Por exemplo, você não pode aplicar [EnableRateLimiting] a OnGet, OnPostou a qualquer outro manipulador de página.

O [DisableRateLimiting] atributo desabilita a limitação de taxa para o controlador, o método de ação ou Razor a Página, independentemente de limitadores de taxa nomeados ou limitadores globais aplicados. Por exemplo, considere o seguinte código que chama RequireRateLimiting para aplicar a limitação de taxa fixedPolicy a todos os endpoints 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.

Métricas de limitação de taxa

O middleware de limitação de taxa fornece métricas internas e recursos de monitoramento para ajudá-lo a entender como os limites de taxa afetam o desempenho do aplicativo e a experiência do usuário. Para obter uma lista de métricas, consulte Microsoft.AspNetCore.RateLimiting.

Testando pontos de extremidade com limitação de taxa

Antes de colocar em produção um aplicativo que usa limitadores de taxa, faça um teste de estresse no aplicativo para validar os limitadores de taxa e as opções utilizadas. Por exemplo, crie um script JMeter usando uma ferramenta como BlazeMeter ou Apache JMeter HTTP(S) Test Script Recorder e carregue o script para Teste de Carga do Azure.

Se você criar partições usando a entrada do usuário, seu aplicativo ficará vulnerável a ataques de DoS ( Negação de Serviço ). Por exemplo, se você criar partições usando endereços IP do cliente, seu aplicativo ficará vulnerável a ataques de Negação de Serviço que usam Falsificação de Endereços de Origem IP. Para obter mais informações, confira BCP 38 RFC 2827 Filtragem de Entrada de Rede: prevenção contra ataques de negação de serviço que utilizam falsificação do endereço IP de origem.

Recursos adicionais