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

Por Arvin Kahbazi, Maarten Balliauwe Rick Anderson

O middleware Microsoft.AspNetCore.RateLimiting fornece middleware de limitação de velocidade. As aplicações configuram políticas de restrição de taxa e, em seguida, associam as políticas aos pontos de extremidade. Os programas que usam limitação de taxa devem ser cuidadosamente testados quanto ao carregamento e revisados antes da implementação. Consulte o artigo Teste de endpoints com limitação de taxa neste artigo para obter mais informações.

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

Por que usar o limite de taxa

O limite de taxa pode ser usado para gerenciar o fluxo de solicitações de entrada para um aplicativo. Principais razões para implementar o limite 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. Esta proteção é particularmente importante para APIs públicas.

  • Garantir o Uso Justo: Ao definir limites que impedem os utilizadores de monopolizarem o sistema, assegura que todos os utilizadores tenham acesso justo aos recursos.

  • Proteção de Recursos: A limitação de taxa ajuda a evitar sobrecarga de servidores ao controlar o número de pedidos que podem ser processados. Protege os recursos do backend de serem sobrecarregados.

  • Reforço da Segurança: Pode mitigar o risco de ataques de Negação de Serviço (DoS) ao limitar a taxa de processamento dos pedidos. Torna mais difícil para os atacantes inundarem um sistema.

  • Melhoria do Desempenho: Ao controlar a taxa de pedidos recebidos, pode manter o desempenho ótimo e a capacidade de resposta de uma aplicação, garantindo uma melhor experiência ao utilizador.

  • Cost Management: 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 limitação de taxa numa aplicação ASP.NET Core pode ajudar a manter estabilidade, segurança e desempenho. O resultado é um serviço fiável e eficiente para todos os utilizadores.

Prevenir ataques DDoS

Embora a limitação de taxa possa ajudar a mitigar o risco de ataques de negação de serviço (DoS), limitando a taxa na qual as solicitações são processadas, não é uma solução abrangente para ataques distribuídos de negação de serviço (DDoS). Os ataques DDoS envolvem vários sistemas sobrecarregando um aplicativo com uma enxurrada de solicitações, dificultando o tratamento apenas com limitação de taxa.

Para uma proteção robusta contra DDoS, considere o uso de um serviço comercial de proteção contra DDoS. Estes serviços oferecem funcionalidades avançadas, tais como:

  • Análise de tráfego: Monitorização e análise contínua do tráfego recebido para detetar e mitigar ataques DDoS em tempo real.
  • Escalabilidade: A capacidade de lidar com ataques em grande escala distribuindo o tráfego em vários servidores e data centers.
  • Mitigação automática: Mecanismos de resposta automatizados para bloquear rapidamente tráfego malicioso 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 monitorizam e atualizam continuamente os 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 do Firewall de Aplicações Web do Azure, AWS Shield ou Google Cloud Armor. As proteções dedicadas estão disponíveis como Web Application Firewalls (WAF) ou como parte de uma solução CDN, como Cloudflare ou Akamai Kona Site Defender

A implementação de um serviço comercial de proteção contra DDoS 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.

Utilize 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. Configure serviços de limitação de velocidade.

No arquivo Program.cs, configure os serviços de limitação de taxa adicionando as políticas de limitação de taxa apropriadas. 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 de ser explicitamente aplicadas às páginas ou endpoints. O exemplo seguinte adiciona uma política de limitador de janela fixa chamada "fixed" que adiciona a um endpoint mais tarde:

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 aplica-se automaticamente a todos os endpoints quando o configuras através das opções. GlobalLimiter.

  1. Ativar intermediário de limitação de taxa

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

app.UseRouting();

app.UseRateLimiter();

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

app.Run();

Aplique políticas de limitação de taxa a terminais ou páginas

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

Aplique uma política atribuída 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 endpoints do controlador:

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

Aplicar limitação de taxa a aplicações Blazor do lado do servidor

Para definir a limitação de taxa para todos os componentes roteáveis de Razor do aplicativo, especifique RequireRateLimiting com o nome da política de limitação de taxa na chamada de 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 através de um ficheiro imports (_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 substitui 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 por meio de um ficheiro de importações, se RequireRateLimitingnão for chamado em MapRazorComponents.

Use o [DisableRateLimiting] atributo para desativar a limitação de taxa para um componente roteável ou uma pasta de componentes através de um ficheiro de importação.

Algoritmos limitadores de taxa

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

Os limitadores fixo, deslizante e token limitam o número máximo de requisições num 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 final ao selecionar um limitador. O custo de um endpoint inclui os recursos utilizados, como tempo, acesso a dados, CPU e I/O.

Limitador de janela fixo

O método AddFixedWindowLimiter usa uma janela de tempo fixa para limitar solicitações. Quando a janela temporal expira, começa uma nova janela e o limite de pedidos é reiniciado.

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 velocidade na coleção de serviços.
  • Chama AddFixedWindowLimiter para criar um limitador de janela fixo com o nome de política "fixed" e define:
  • PermitLimit para 4 e o tempo Window para 12. É permitido um máximo de 4 pedidos por cada janela de 12 segundos.
  • QueueProcessingOrder para OldestFirst.
  • QueueLimit a 2 (defina isto como 0 para desativar o mecanismo de enfileiramento).
  • Invoca UseRateLimiter para ativar a limitação de taxa.

As aplicações 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 APIs específicas de limite 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 fixo, mas adiciona segmentos por janela. A janela desliza um segmento a cada intervalo. O intervalo de segmento é (tempo de janela)/(segmentos por janela).
  • Limita as solicitações para uma janela a permitLimit solicitações.
  • Cada janela de tempo é dividida em n segmentos por janela.
  • As solicitações retiradas do segmento de tempo expirado uma janela atrás (n segmentos anteriores ao segmento atual) são adicionadas ao segmento atual. Referimo-nos ao segmento de tempo mais expirado de uma janela anterior 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 linha superior e a primeira coluna mostram o segmento de tempo.
  • A segunda linha mostra as solicitações restantes disponíveis. Os restantes pedidos são calculados como os pedidos disponíveis menos os pedidos processados mais os pedidos reciclados.
  • As solicitações em cada momento movem-se ao longo da linha azul diagonal.
  • A partir do tempo 30, a solicitação retirada do segmento de tempo expirado é adicionada de volta ao limite de solicitação, conforme mostrado nas linhas vermelhas.

Tabela mostrando solicitações, limites e slots reciclados

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

Hora Disponível Tomado Reciclado a partir de materiais expirados Transferência
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 com 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 balde de tokens

O limitador de balde de tokens é semelhante ao limitador de janela deslizante, mas, em vez de adicionar novamente as solicitações retiradas 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 superior ao limite do balde de tokens. 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 Tomado Adicionado Transferência
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 está definido como true, um temporizador interno reabastece os tokens a cada ReplenishmentPeriod; quando definido como false, a aplicação deve chamar TryReplenish para o 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 limitadoras de taxa

As partições de limitação de taxa dividem o tráfego em grupos separados, cada um com os seus próprios contadores de limitação de taxa. Esta abordagem proporciona um controlo mais granular do que um único contador global. Diferentes chaves, como ID de utilizador, endereço IP ou chave API, definem os buckets de partição.

Benefícios do particionamento

  • Justiça: Um utilizador não pode consumir todo o limite de taxa para todos.
  • Granularidade: Limites diferentes para diferentes utilizadores e recursos.
  • Segurança: Melhor proteção contra abusos direcionados.
  • Serviço em níveis: Suporte para níveis de serviço com diferentes limites.

A limitação de taxa particionada oferece controle refinado sobre como você gerencia o tráfego da 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 utilizador

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 final

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 múltiplas PartitionedRateLimiter instâncias e combina-as numa só PartitionedRateLimiter. O limitador combinado executa todos os limitadores de entrada em sequência. Como o resultado é atribuído PartitionedRateLimiter a GlobalLimiter, a cadeia aplica-se a todos os pontos finais. Para encadear limitadores para um ponto final específico, em vez disso, utilize uma política com nome, conforme mostrado em Encadear limitadores numa política com nome.

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 mais informações, consulte o código-fonte CreateChained.

Limitadores de encadeamento numa política nomeada

CreateChained encadeia limitadores globais que se aplicam a todos os pontos de extremidade. Para combinar vários tipos de limitadores e aplicá-los a pontos finais específicos, encadeie os limitadores numa política nomeada com CreateChained. Esta sobrecarga devolve um único RateLimiter que executa cada limitador em sequência, que é o tipo de retorno que a fábrica de partições de uma política nomeada requer.

A política seguinte "combined" encadeia um limitador token bucket e um limitador de concorrência com RateLimiter.CreateChained e, em seguida, aplica a política a um único ponto final 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");

Um pedido tem de obter uma concessão de cada limitador da cadeia para prosseguir, e os limitadores são executados pela ordem em que são passados para RateLimiter.CreateChained. Se um limitador rejeitar o pedido, o pedido é rejeitado e os arrendamentos já adquiridos de limitadores anteriores na cadeia são distribuídos por ordem inversa.

Ao encadear limitadores numa apólice nomeada, tenha o seguinte em mente:

  • Ao libertar uma concessão, a permissão é devolvida a um limitador de concorrência. Os limitadores baseados em tempo (balde de token, janela fixa e janela deslizante) não devolvem uma licença já obtida quando um limitador posterior na cadeia rejeita o pedido, por isso tem isto em conta ao encomendar os limitadores na cadeia.
  • Criar um TokenBucketRateLimiter diretamente com AutoReplenishment definido como true dá a cada instância do limitador o seu próprio temporizador. Os auxiliares AddTokenBucketLimiter e RateLimitPartition.GetTokenBucketLimiter definem, em vez disso, AutoReplenishment para false e repõem todos os seus limitadores a partir de um único temporizador partilhado.
  • RateLimiter.CreateChained não elimina os limitadores que lhe são passados. No exemplo anterior, a partição armazena em cache o limitador encadeado e o framework gere a sua vida útil. Se criares limitadores em cadeia fora de uma partição, elimina os limitadores internos quando deixarem de ser usados.
  • Prefiro a abordagem global PartitionedRateLimiter.CreateChained em que a cadeia deve aplicar-se a todos os endpoints. Utilize uma política nomeada com RateLimiter.CreateChained apenas quando a cadeia tiver de ser limitada a endpoints específicos.

Escolher o que acontece quando um pedido é sujeito a limitação de taxa

Para casos simples, você pode apenas 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 é registar um OnRejected callback ao configurar o limite 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:

Fila de pedidos

Quando ativas a fila, se um pedido ultrapassar o limite de taxa, o sistema coloca-o numa fila. A solicitação fica na fila até que uma permissão fique disponível ou ocorra um tempo limite. O sistema processa pedidos 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
    });
});

EnableRateLimiting e DisableRateLimiting atributos

Aplique os atributos [EnableRateLimiting] e [DisableRateLimiting] a um controlador, método de ação ou página Razor. Para Razor Páginas, aplica o atributo à Razor Página e não aos manipuladores de página. Por exemplo, não é possível aplicar [EnableRateLimiting] a OnGet, OnPost ou a qualquer outro processador de páginas.

O [DisableRateLimiting] atributo desativa a limitação de taxa para o controlador, método de ação ou Razor Página, independentemente dos limitadores de taxa nomeados ou dos limitadores globais aplicados. Por exemplo, considere o código a seguir que chama RequireRateLimiting para aplicar a limitação de taxa de 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] desativa o limite de taxa e substitui o [EnableRateLimiting("fixed")] aplicado ao Home2Controller e ao app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) chamado 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, [EnableRateLimiting("sliding")] não é aplicado ao método de ação Privacy porque Program.cs chama 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.
  • O limite de taxa está desativado no método de ação NoLimit.

Métricas de limitação de taxa

O middleware de limitação de taxa fornece métricas incorporadas e capacidades de monitorização para o ajudar a compreender como os limites de taxa afetam o desempenho da aplicação e a experiência do utilizador. Para uma lista de métricas, veja Microsoft.AspNetCore.RateLimiting.

Testes de endpoints com limitação de taxa de transferência

Antes de implementar uma aplicação que use limitação de taxa para produção, teste a aplicação para validar os limitadores de taxa e as opções que usaste. Por exemplo, crie um script JMeter usando uma ferramenta como o BlazeMeter ou o Apache JMeter HTTP(S) Test Script Recorder e carregue o script no Azure Load Testing.

Se criar partições com base em entradas do utilizador, a sua aplicação torna-se vulnerável a ataques de negação de serviço (DoS). Por exemplo, se criar partições usando endereços IP do cliente, a sua aplicação torna-se vulnerável a ataques de Negação de Serviço que utilizam falsificação de endereços de origem IP. Para obter mais informações, consulte BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service Attacks que empregam IP Source Address Spoofing.

Recursos adicionais