Partilhar via


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 aplicativos que usam limitação de taxa devem ser cuidadosamente carregados, testados e revisados antes da implantação. Consulte o artigo Teste de endpoints com limitação de taxa neste artigo para obter mais informações.

Para obter uma introdução ao limite de taxa, consulte Rate limitando middleware.

Algoritmos limitadores de taxa

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

Limitador de janela fixo

O método AddFixedWindowLimiter usa uma janela de tempo fixa para limitar 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 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 a 4 e o tempo Window a 12. É permitido um máximo de 4 pedidos por cada janela de 12 segundos.
  • QueueProcessingOrder para OldestFirst.
  • QueueLimit a 2.
  • 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 uma janela atrás 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 a cada vez se movem 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 (O Transitar da linha anterior). A primeira linha mostra 100 solicitações disponíveis porque não há segmento anterior.

Hora Disponível Tomadas 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 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 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 maior do que o limite de bucket 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 Tomadas Adicionado Transição
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 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 a passagem de vários PartitionedRateLimiter que são combinados num 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 = 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'

EnableRateLimiting e DisableRateLimiting atributos

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

O atributo [DisableRateLimiting]desabilita limite de taxa para o controlador, método de ação ou página Razor, independentemente dos limitadores de taxa nomeados ou dos limitadores globais aplicados. Por exemplo, considere o código a seguir que chama RequireRateLimiting para aplicar o limite de taxa de 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] 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, o [EnableRateLimiting("sliding")]não é aplicado ao método de ação Privacy porque Program.cs chamado 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.

Aplicando atributos a páginas Razor

Para Razor Pages, o atributo deve ser aplicado ao Razor Page e não aos manipuladores de página. Por exemplo, [EnableRateLimiting] não pode ser aplicado a OnGet, OnPostou qualquer outro manipulador de página.

O atributo DisableRateLimiting desativa o limite de taxa em uma Página Razor. EnableRateLimiting só é aplicado a uma Página Razor se MapRazorPages().RequireRateLimiting(Policy)não tiver sido chamado.

Comparação de algoritmos limitadores

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. O custo de um ponto final deve ser considerado ao selecionar um limitador. O custo de um endpoint inclui os recursos utilizados, por exemplo, tempo, acesso a dados, CPU e I/O.

Amostras de limitadores de taxa

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

Limitador com OnRejected, RetryAftere GlobalLimiter

A seguinte amostra:

  • Cria uma função de retorno RateLimiterOptions.OnRejected que é chamada quando uma solicitação excede o limite especificado. retryAfter pode ser usado com o TokenBucketRateLimiter, FixedWindowLimitere 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 é mostrada posteriormente neste artigo.
    • Uma SlidingWindowLimiter:
      • Com uma partição para cada usuário autenticado.
      • Uma partição partilhada para todos os utilizadores 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 existir. O GlobalLimiter cria uma partição para cada IPAddress.
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.Identity.Name;
        }

        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.UseAuthentication();
app.UseRateLimiter(); // important to add after UseAuthentication because the limiter uses auth info
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();

Advertência

A criação de partições em endereços IP de clientes torna o aplicativo vulnerável a ataques de negação de serviço que empregam falsificação de endereço de origem IP. Para obter mais informações, consulte BCP 38 RFC 2827 Filtragem de Ingressão de Rede: Combate aos Ataques de Negação de Serviço que utilizam Falsificação de Endereço IP de Origem.

Consulte o repositório de amostras para 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 o status de resposta como 429 Too Many Requests. O status rejeitado padrão é 503 Serviço Indisponível.

Limitador com autorização

O exemplo a seguir usa JSON Web Tokens (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 atuando 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, TokenBucketRateLimitere autorização

A seguinte amostra:

  • Adiciona um ConcurrencyLimiter com o nome da política "get" que é usado nas páginas Razor.
  • Adiciona um TokenBucketRateLimiter com uma partição para cada usuário autorizado e uma partição para todos os usuários anônimos.
  • Define RateLimiterOptions.RejectionStatusCode para 429 Too Many Requests.
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
            });
    }));

Consulte repositório de amostras para obter o arquivo Program.cs completo.

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

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

A criação de partições a partir da inserção do utilizador torna o aplicativo vulnerável a ataques de de negação de serviço (DoS). Por exemplo, a criação de partições em endereços IP de clientes torna o aplicativo vulnerável a ataques de negação de serviço que empregam 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