Middleware de limitação de taxa no ASP.NET Core
Por Arvin Kahbazi, Maarten Balliauw e Rick Anderson
O middleware Microsoft.AspNetCore.RateLimiting
fornece middleware de limitação de taxa. Os aplicativos configuram políticas de limitação de taxa e anexam as políticas aos pontos de extremidade. Os aplicativos que usam a limitação de taxa devem ser cuidadosamente testados e revisados antes da implantação. Confira Testando pontos de extremidade com limitação de taxa neste artigo para obter mais informações.
Para obter uma introdução à limitação de taxa, confira Middleware de limitação de taxa.
Algoritmos de limitador de taxa
A classe RateLimiterOptionsExtensions
fornece os seguintes métodos de extensão para limitação de taxa:
Limitador de janela fixa
O método AddFixedWindowLimiter
usa uma janela de tempo fixa para limitar as solicitações. Quando a janela de tempo expira, uma nova janela de tempo é iniciada e o limite de solicitação é redefinido.
Considere o seguinte código:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: "fixed", options =>
{
options.PermitLimit = 4;
options.Window = TimeSpan.FromSeconds(12);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 2;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
.RequireRateLimiting("fixed");
app.Run();
O código anterior:
- Chama AddRateLimiter para adicionar um serviço de limitação de taxa à coleção de serviços.
- Chama
AddFixedWindowLimiter
para criar um limitador de janela fixa com um nome de política"fixed"
e define: - PermitLimit como 4 e o tempo Window como 12. Um máximo de 4 solicitações por cada janela de 12 segundos são permitidas.
- QueueProcessingOrder para OldestFirst.
- QueueLimit para 2.
- Chama UseRateLimiter para habilitar a limitação de taxa.
Os aplicativos devem usar Configuração para definir opções de limitador. O código a seguir atualiza o código anterior usando MyRateLimitOptions
para configuração:
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Fixed Window Limiter {GetTicks()}"))
.RequireRateLimiting(fixedPolicy);
app.Run();
UseRateLimiter deve ser chamado após UseRouting
quando as APIs específicas do ponto de extremidade de limitação de taxa são usadas. Por exemplo, se o atributo [EnableRateLimiting]
for usado, UseRateLimiter
deverá ser chamado após UseRouting
. Ao chamar apenas limitadores globais, UseRateLimiter
pode ser chamado antes de UseRouting
.
Limitador de janela deslizante
Um algoritmo de janela deslizante:
- É semelhante ao limitador de janela fixa, mas adiciona segmentos por janela. A janela desliza um segmento a cada intervalo de segmento. O intervalo do segmento é (tempo da janela)/(segmentos por janela).
- Limita as solicitações de uma janela a solicitações
permitLimit
. - Cada janela de tempo é dividida em
n
segmentos por janela. - As solicitações retiradas do segmento de tempo expirado de uma janela (
n
segmentos anteriores ao segmento atual) são adicionadas ao segmento atual. Nos referimos ao segmento de tempo mais expirado uma janela de volta como o segmento expirado.
Considere a tabela a seguir, que mostra um limitador de janela deslizante com uma janela de 30 segundos, três segmentos por janela e um limite de 100 solicitações:
- A primeira linha e a primeira coluna mostram o segmento de tempo.
- A segunda linha mostra as solicitações restantes disponíveis. As solicitações restantes são calculadas como as solicitações disponíveis menos as solicitações processadas mais as solicitações recicladas.
- As solicitações em cada vez se movem ao longo da linha azul diagonal.
- A partir do tempo 30 em diante, a solicitação feita do segmento de tempo expirado é adicionada de volta ao limite de solicitação, conforme mostrado nas linhas vermelhas.
A tabela a seguir mostra os dados no grafo anterior em um formato diferente. A coluna Disponível mostra as solicitações disponíveis do segmento anterior (O Transporte da linha anterior). A primeira linha mostra 100 disponíveis porque não há nenhum segmento anterior.
Tempo | Disponível | Obtido | Reciclado de expirado | Carregar |
---|---|---|---|---|
0 | 100 | 20 | 0 | 80 |
10 | 80 | 30 | 0 | 50 |
20 | 50 | 40 | 0 | 10 |
30 | 10 | 30 | 20 | 0 |
40 | 0 | 10 | 30 | 20 |
50 | 20 | 10 | 40 | 50 |
60 | 50 | 35 | 30 | 45 |
O código a seguir usa o limitador de taxa de janela deslizante:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Sliding Window Limiter {GetTicks()}"))
.RequireRateLimiting(slidingPolicy);
app.Run();
Limitador de bucket de token
O limitador de bucket de token é semelhante ao limitador de janela deslizante, mas em vez de adicionar de volta as solicitações feitas do segmento expirado, um número fixo de tokens é adicionado a cada período de reabastecimento. Os tokens adicionados a cada segmento não podem aumentar os tokens disponíveis para um número maior que o limite do bucket de token. A tabela a seguir mostra um limitador de bucket de token com um limite de 100 tokens e um período de reabastecimento de 10 segundos.
Tempo | Disponível | Obtido | Adicionado | Carregar |
---|---|---|---|---|
0 | 100 | 20 | 0 | 80 |
10 | 80 | 10 | 20 | 90 |
20 | 90 | 5 | 15 | 100 |
30 | 100 | 30 | 20 | 90 |
40 | 90 | 6 | 16 | 100 |
50 | 100 | 40 | 20 | 80 |
60 | 80 | 50 | 20 | 50 |
O código a seguir usa o limitador de bucket de token:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var tokenPolicy = "token";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddTokenBucketLimiter(policyName: tokenPolicy, options =>
{
options.TokenLimit = myOptions.TokenLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
options.ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod);
options.TokensPerPeriod = myOptions.TokensPerPeriod;
options.AutoReplenishment = myOptions.AutoReplenishment;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}"))
.RequireRateLimiting(tokenPolicy);
app.Run();
Quando AutoReplenishment é definido como true
, um temporizador interno reabastece os tokens a cada ReplenishmentPeriod; quando definido como false
, o aplicativo deve chamar TryReplenish no limitador.
Limitador de simultaneidade
O limitador de simultaneidade limita o número de solicitações simultâneas. Cada solicitação reduz o limite de simultaneidade em um. Quando uma solicitação é concluída, o limite é aumentado em um. Ao contrário dos outros limitadores de solicitações que limitam o número total de solicitações para um período especificado, o limitador de simultaneidade limita apenas o número de solicitações simultâneas e não limita o número de solicitações em um período de tempo.
O código a seguir usa o limitador de simultaneidade:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var concurrencyPolicy = "Concurrency";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddConcurrencyLimiter(policyName: concurrencyPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", async () =>
{
await Task.Delay(500);
return Results.Ok($"Concurrency Limiter {GetTicks()}");
}).RequireRateLimiting(concurrencyPolicy);
app.Run();
Criar limitadores encadeados
A API CreateChained permite passar vários PartitionedRateLimiter que são combinados em um PartitionedRateLimiter
. O limitador combinado executa todos os limitadores de entrada em sequência.
O código a seguir usa CreateChained
:
using System.Globalization;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(_ =>
{
_.OnRejected = (context, _) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");
return new ValueTask();
};
_.GlobalLimiter = PartitionedRateLimiter.CreateChained(
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 4,
Window = TimeSpan.FromSeconds(2)
});
}),
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 20,
Window = TimeSpan.FromSeconds(30)
});
}));
});
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"));
app.Run();
Para obter mais informações, confira o código-fonte CreateChained
Atributos EnableRateLimiting
e DisableRateLimiting
Os atributos [EnableRateLimiting]
e [DisableRateLimiting]
podem ser aplicados a um Controlador, método de ação ou página Razor. Para páginas Razor, o atributo deve ser aplicado à página Razor e não aos manipuladores de página. Por exemplo, [EnableRateLimiting]
não pode ser aplicado a OnGet
, OnPost
ou a qualquer outro manipulador de página.
O atributo [DisableRateLimiting]
desabilita a limitação de taxa ao Controlador, ao método de ação ou à página Razor, independentemente dos limitadores de taxa nomeados ou dos limitadores globais aplicados. Por exemplo, considere o seguinte código que chama RequireRateLimiting para aplicar a limitação de taxa fixedPolicy
a todos os pontos de extremidade do controlador:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages().RequireRateLimiting(slidingPolicy);
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy);
app.Run();
No código a seguir, [DisableRateLimiting]
desabilita a limitação de taxa e substitui o [EnableRateLimiting("fixed")]
aplicado ao Home2Controller
e app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
chamados em Program.cs
:
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}
public ActionResult Index()
{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
No código anterior, o [EnableRateLimiting("sliding")]
não é aplicado ao método de ação Privacy
porque Program.cs
chamou app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
.
Considere o seguinte código que não chama RequireRateLimiting
em MapRazorPages
ou MapDefaultControllerRoute
:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages();
app.MapDefaultControllerRoute(); // RequireRateLimiting not called
app.Run();
Considere o seguinte controlador:
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}
public ActionResult Index()
{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
No controlador anterior:
- O limitador de taxa de política
"fixed"
é aplicado a todos os métodos de ação que não têm atributosEnableRateLimiting
eDisableRateLimiting
. - O limitador de taxa de política
"sliding"
é aplicado à açãoPrivacy
. - A limitação de taxa está desabilitada no método de ação
NoLimit
.
Aplicando atributos a páginas Razor
Para páginas Razor, o atributo deve ser aplicado à página Razor e não aos manipuladores de página. Por exemplo, [EnableRateLimiting]
não pode ser aplicado a OnGet
, OnPost
ou a qualquer outro manipulador de página.
O atributo DisableRateLimiting
desabilita a limitação de taxa em uma página Razor. EnableRateLimiting
só será aplicado a uma página Razor se MapRazorPages().RequireRateLimiting(Policy)
não tiver sido chamado.
Comparação de algoritmos de limitador
Os limitadores fixos, deslizantes e de token limitam o número máximo de solicitações em um período de tempo. O limitador de simultaneidade limita apenas o número de solicitações simultâneas e não limita o número de solicitações em um período de tempo. O custo de um ponto de extremidade deve ser considerado ao selecionar um limitador. O custo de um ponto de extremidade inclui os recursos usados – por exemplo, tempo, acesso a dados, CPU e E/S.
Exemplos de limitador de taxa
Os exemplos a seguir não são destinados ao código de produção, mas são exemplos de como usar os limitadores.
Limitador com OnRejected
, RetryAfter
e GlobalLimiter
O exemplo a seguir:
Cria um retorno de chamada RateLimiterOptions.OnRejected que é chamado quando uma solicitação excede o limite especificado.
retryAfter
pode ser usado com o TokenBucketRateLimiter , FixedWindowLimiter , e SlidingWindowLimiter , pois esses algoritmos conseguem estimar quando mais permissões serão adicionadas. OConcurrencyLimiter
não tem como calcular quando as licenças estarão disponíveis.Adiciona os seguintes limitadores:
- Um
SampleRateLimiterPolicy
que implementa a interfaceIRateLimiterPolicy<TPartitionKey>
. A classeSampleRateLimiterPolicy
é exibida posteriormente neste tópico. - Um
SlidingWindowLimiter
:- Com uma partição para cada usuário autenticado.
- Uma partição compartilhada para todos os usuários anônimos.
- Um GlobalLimiter que é aplicado a todas as solicitações. O limitador global será executado primeiro, seguido pelo limitador específico do ponto de extremidade, se houver. O
GlobalLimiter
cria uma partição para cada IPAddress.
- Um
// Preceding code removed for brevity.
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var userPolicyName = "user";
var helloPolicy = "hello";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(limiterOptions =>
{
limiterOptions.OnRejected = (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
.CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
.LogWarning("OnRejected: {GetUserEndPoint}", GetUserEndPoint(context.HttpContext));
return new ValueTask();
};
limiterOptions.AddPolicy<string, SampleRateLimiterPolicy>(helloPolicy);
limiterOptions.AddPolicy(userPolicyName, context =>
{
var username = "anonymous user";
if (context.User.Identity?.IsAuthenticated is true)
{
username = context.User.ToString()!;
}
return RateLimitPartition.GetSlidingWindowLimiter(username,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = myOptions.PermitLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
Window = TimeSpan.FromSeconds(myOptions.Window),
SegmentsPerWindow = myOptions.SegmentsPerWindow
});
});
limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, IPAddress>(context =>
{
IPAddress? remoteIpAddress = context.Connection.RemoteIpAddress;
if (!IPAddress.IsLoopback(remoteIpAddress!))
{
return RateLimitPartition.GetTokenBucketLimiter
(remoteIpAddress!, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}
return RateLimitPartition.GetNoLimiter(IPAddress.Loopback);
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages().RequireRateLimiting(userPolicyName);
app.MapDefaultControllerRoute();
static string GetUserEndPoint(HttpContext context) =>
$"User {context.User.Identity?.Name ?? "Anonymous"} endpoint:{context.Request.Path}"
+ $" {context.Connection.RemoteIpAddress}";
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
.RequireRateLimiting(userPolicyName);
app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
.RequireRateLimiting(helloPolicy);
app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}");
app.Run();
Aviso
A criação de partições em endereços IP do cliente torna o aplicativo vulnerável a ataques de negação de serviço que empregam falsificação de endereço IP de origem. Para obter mais informações, confira Filtragem de entrada de rede DO BCP 38 RFC 2827: derrotar ataques de negação de serviço que empregam falsificação de endereço IP de origem.
Confira o repositório de exemplos para obter o arquivo Program.cs
completo.
A classe SampleRateLimiterPolicy
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using WebRateLimitAuth.Models;
namespace WebRateLimitAuth;
public class SampleRateLimiterPolicy : IRateLimiterPolicy<string>
{
private Func<OnRejectedContext, CancellationToken, ValueTask>? _onRejected;
private readonly MyRateLimitOptions _options;
public SampleRateLimiterPolicy(ILogger<SampleRateLimiterPolicy> logger,
IOptions<MyRateLimitOptions> options)
{
_onRejected = (ctx, token) =>
{
ctx.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
logger.LogWarning($"Request rejected by {nameof(SampleRateLimiterPolicy)}");
return ValueTask.CompletedTask;
};
_options = options.Value;
}
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected => _onRejected;
public RateLimitPartition<string> GetPartition(HttpContext httpContext)
{
return RateLimitPartition.GetSlidingWindowLimiter(string.Empty,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = _options.PermitLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = _options.QueueLimit,
Window = TimeSpan.FromSeconds(_options.Window),
SegmentsPerWindow = _options.SegmentsPerWindow
});
}
}
No código anterior, OnRejected usa OnRejectedContext para definir a resposta status como 429 Excesso de solicitações. O padrão rejeitado status é 503 Serviço indisponível.
Limitador com autorização
O exemplo a seguir usa JWTs (Tokens Web JSON) e cria uma partição com o token de acesso JWT. Em um aplicativo de produção, o JWT normalmente seria fornecido por um servidor que atua como um serviço de token de segurança (STS). Para desenvolvimento local, a ferramenta de linha de comando dotnet user-jwts pode ser usada para criar e gerenciar JWTs locais específicos do aplicativo.
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Primitives;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var jwtPolicyName = "jwt";
builder.Services.AddRateLimiter(limiterOptions =>
{
limiterOptions.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
limiterOptions.AddPolicy(policyName: jwtPolicyName, partitioner: httpContext =>
{
var accessToken = httpContext.Features.Get<IAuthenticateResultFeature>()?
.AuthenticateResult?.Properties?.GetTokenValue("access_token")?.ToString()
?? string.Empty;
if (!StringValues.IsNullOrEmpty(accessToken))
{
return RateLimitPartition.GetTokenBucketLimiter(accessToken, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}
return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = true
});
});
});
var app = builder.Build();
app.UseAuthorization();
app.UseRateLimiter();
app.MapGet("/", () => "Hello, World!");
app.MapGet("/jwt", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
.RequireRateLimiting(jwtPolicyName)
.RequireAuthorization();
app.MapPost("/post", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
.RequireRateLimiting(jwtPolicyName)
.RequireAuthorization();
app.Run();
static string GetUserEndPointMethod(HttpContext context) =>
$"Hello {context.User.Identity?.Name ?? "Anonymous"} " +
$"Endpoint:{context.Request.Path} Method: {context.Request.Method}";
Limitador com ConcurrencyLimiter
, TokenBucketRateLimiter
e autorização
O exemplo a seguir:
- Adiciona um
ConcurrencyLimiter
com um nome de política de"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 como 429 Excesso de solicitações.
var getPolicyName = "get";
var postPolicyName = "post";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddConcurrencyLimiter(policyName: getPolicyName, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
})
.AddPolicy(policyName: postPolicyName, partitioner: httpContext =>
{
string userName = httpContext.User.Identity?.Name ?? string.Empty;
if (!StringValues.IsNullOrEmpty(userName))
{
return RateLimitPartition.GetTokenBucketLimiter(userName, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}
return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = true
});
}));
Confira o repositório de exemplos para obter o arquivo Program.cs
completo.
Testando pontos de extremidade com limitação de taxa
Antes de implantar um aplicativo à produção usando a limitação de taxa, teste o aplicativo para validar os limitadores de taxa e as opções usadas. Por exemplo, crie um script JMeter com uma ferramenta como o BlazeMeter ou o Gravador de Script de Teste HTTP(S) do Apache JMeter e carregue o script no Teste de Carga do Azure.
A criação de partições com entrada do usuário torna o aplicativo vulnerável a ataques de Negação de Serviço (DoS). Por exemplo, a criação de partições em endereços IP do cliente torna o aplicativo vulnerável a ataques de negação de serviço que empregam falsificação de endereço IP de origem. Para obter mais informações, confira Filtragem de entrada de rede DO BCP 38 RFC 2827: derrotar ataques de negação de serviço que empregam falsificação de endereço IP de origem.
Recursos adicionais
- O middleware de limitação de taxa por Maarten Balliauw fornece uma excelente introdução e visão geral da limitação de taxa.
- Limitar a taxa de um manipulador HTTP no .NET