ПО промежуточного слоя для ограничения скорости в ASP.NET Core
Арвин Кахбази, Маартен Бальяув, и Рик Андерсон
ПО промежуточного слоя Microsoft.AspNetCore.RateLimiting
предоставляет ПО промежуточного слоя для ограничения скорости. Приложения настраивают политики ограничения скорости, а затем присоединяют политики к конечным точкам. Приложения, использующие ограничение скорости, должны тщательно тестироваться и проверяться перед развертыванием. Дополнительные сведения см. в разделе "Тестирование конечных точек с ограничением скорости " в этой статье.
Введение в ограничение скорости см. в разделе "Ограничение скорости" по промежуточного слоя.
Алгоритмы ограничения скорости
Класс RateLimiterOptionsExtensions
предоставляет следующие методы расширения для ограничения скорости:
Исправлено ограничение окна
Метод AddFixedWindowLimiter
использует фиксированное время для ограничения запросов. Когда истекает срок действия периода времени, запускается новое окно времени, а ограничение запроса сбрасывается.
Рассмотрим следующий код:
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();
Предыдущий код:
- Вызовы AddRateLimiter для добавления службы ограничения скорости в коллекцию служб.
- Вызовы
AddFixedWindowLimiter
для создания фиксированного ограничения окна с именем политики и наборами"fixed"
: - PermitLimit до 4 и времени Window до 12. Допускается не более 4 запросов в каждом 12-секундном окне.
- QueueProcessingOrder изменено на OldestFirst.
- QueueLimit до 2.
- Вызовы UseRateLimiter для включения ограничения скорости.
Приложения должны использовать конфигурацию для задания параметров ограничения. Следующий код обновляет предыдущий код, используя MyRateLimitOptions
для настройки:
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 необходимо вызывать после того, UseRouting
как используются api ограничения скорости ограничения скорости для конкретной конечной точки. Например, если [EnableRateLimiting]
используется атрибут, UseRateLimiter
необходимо вызвать после UseRouting
. При вызове только глобальных ограничений UseRateLimiter
можно вызвать раньше UseRouting
.
Ограничение скользящего окна
Алгоритм скользящего окна:
- Аналогично фиксированному ограничению окна, но добавляет сегменты в окно. Окно слайдирует по одному сегменту каждый интервал сегмента. Интервал сегмента — (время окна)/(сегменты на окно).
- Ограничивает запросы окна запросами
permitLimit
. - Каждый период времени делится на сегменты на
n
окно. - Запросы, полученные из сегмента истекшим сроком действия, возвращаются в текущий сегмент (
n
сегменты до текущего сегмента). Мы называем наиболее просроченным сегментом времени в одном окне назад в качестве сегмента с истекшим сроком действия.
Рассмотрим следующую таблицу с скользящим ограничением окна с 30-секундным окном, тремя сегментами в окне и ограничением в 100 запросов:
- Верхняя строка и первый столбец показывают сегмент времени.
- Вторая строка показывает оставшиеся доступные запросы. Остальные запросы вычисляются как доступные запросы минус обработанные запросы, а также переработанные запросы.
- Запросы каждый раз перемещаются по диагонали синей линии.
- По истечении 30 раз запрос, полученный из сегмента времени с истекшим сроком действия, добавляется обратно в предел запроса, как показано в красных строках.
В следующей таблице показаны данные в предыдущем графе в другом формате. В столбце "Доступно" отображаются запросы, доступные из предыдущего сегмента (Перенос из предыдущей строки). В первой строке отображается 100 доступных запросов, так как предыдущий сегмент отсутствует.
Время | На месте | Взятый | Истек срок действия повторного использования | Переносить |
---|---|---|---|---|
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 |
В следующем коде используется ограничение скорости скользящего окна:
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();
Ограничение сегментов маркеров
Ограничение контейнеров маркеров аналогично скользящему ограничению окна, но вместо добавления запросов, полученных из сегмента с истекшим сроком действия, добавляется фиксированное число маркеров каждый период пополнения. Маркеры, добавленные каждый сегмент, не могут увеличить доступные маркеры до числа выше предела контейнера маркеров. В следующей таблице показан ограничивающий контейнер маркеров с ограничением в 100 маркеров и периодом пополнения в 10 секунд.
Время | На месте | Взятый | Добавлено | Переносить |
---|---|---|---|---|
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 |
В следующем коде используется средство ограничения контейнера маркеров:
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();
Если AutoReplenishment задано значение true
, внутренний таймер обновляет маркеры каждые ReplenishmentPeriod; при установке false
приложение должно вызывать TryReplenish ограничение.
Ограничение параллелизма
Ограничение параллелизма ограничивает количество одновременных запросов. Каждый запрос уменьшает ограничение параллелизма на один. По завершении запроса ограничение увеличивается на один. В отличие от других ограничений запросов, ограничивающих общее количество запросов в течение указанного периода, ограничение параллелизма ограничивает только количество одновременных запросов и не ограничивает количество запросов за определенный период времени.
В следующем коде используется ограничение параллелизма:
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();
Создание ограничивающих элементов в цепочке
CreateChained API позволяет передавать несколькоPartitionedRateLimiter, которые объединяются в одинPartitionedRateLimiter
. Объединенный ограничитель выполняет все входные ограничения в последовательности.
В приведенном ниже коде используется 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();
Дополнительные сведения см. в исходном коде CreateChained
Атрибуты EnableRateLimiting
и DisableRateLimiting
[DisableRateLimiting]
Атрибуты [EnableRateLimiting]
могут применяться к контроллеру, методу действия или Razor странице. Для Razor Pages атрибут должен применяться к Razor странице, а не к обработчикам страниц. Например, [EnableRateLimiting]
нельзя применять OnGet
к обработчику OnPost
страниц или к любому другому обработчику страницы.
Атрибут [DisableRateLimiting]
отключает ограничение скорости контроллера, метода действия или Razor страницы независимо от примененных ограничений именованных ограничений скорости или глобальных ограничений. Например, рассмотрим следующий код, который вызывает RequireRateLimiting применение fixedPolicy
ограничения скорости ко всем конечным точкам контроллера:
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();
В следующем коде [DisableRateLimiting]
отключает ограничение скорости и переопределяет, примененные [EnableRateLimiting("fixed")]
к вызываемой Home2Controller
Program.cs
функцииapp.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
:
[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 });
}
}
В приведенном выше коде не применяется к методу Privacy
действия, [EnableRateLimiting("sliding")]
так как Program.cs
вызываетсяapp.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
.
Рассмотрим следующий код, который не вызывается RequireRateLimiting
MapRazorPages
или 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();
Рассмотрим следующий контроллер:
[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 });
}
}
На предыдущем контроллере:
- Ограничение
"fixed"
скорости политики применяется ко всем методам действий, которые не имеютEnableRateLimiting
иDisableRateLimiting
атрибуты. - Ограничение
"sliding"
скорости политики применяется кPrivacy
действию. - Ограничение скорости отключено в методе
NoLimit
действия.
Применение атрибутов к страницам Razor
Для Razor Pages атрибут должен применяться к Razor странице, а не к обработчикам страниц. Например, [EnableRateLimiting]
нельзя применять OnGet
к обработчику OnPost
страниц или к любому другому обработчику страницы.
Атрибут DisableRateLimiting
отключает ограничение скорости на Razor странице. EnableRateLimiting
применяется только к Razor странице, если MapRazorPages().RequireRateLimiting(Policy)
не был вызван.
Сравнение алгоритмов ограничения
Фиксированные, скользящие и маркерные ограничения ограничивают максимальное количество запросов за определенный период времени. Ограничение параллелизма ограничивает только количество одновременных запросов и не ограничивает количество запросов за период времени. При выборе ограничения следует учитывать стоимость конечной точки. Стоимость конечной точки включает ресурсы, используемые, например, время, доступ к данным, ЦП и ввода-вывода.
Примеры ограничения скорости
Следующие примеры не предназначены для рабочего кода, но являются примерами использования ограничивающих элементов.
Предельный с OnRejected
, RetryAfter
и GlobalLimiter
Следующий пример:
Создает обратный вызов RateLimiterOptions.OnRejected , который вызывается, когда запрос превышает указанное ограничение.
retryAfter
можно использовать с параметромTokenBucketRateLimiter
,FixedWindowLimiter
иSlidingWindowLimiter
так как эти алгоритмы могут оценить, когда будут добавлены дополнительные разрешения. НетConcurrencyLimiter
способа вычисления, когда разрешения будут доступны.Добавляет следующие ограничения:
- Объект,
SampleRateLimiterPolicy
реализующийIRateLimiterPolicy<TPartitionKey>
интерфейс. КлассSampleRateLimiterPolicy
показан далее в этой статье. - A
SlidingWindowLimiter
:- С секцией для каждого пользователя, прошедшего проверку подлинности.
- Одна общая секция для всех анонимных пользователей.
- Применяется GlobalLimiter ко всем запросам. Глобальный ограничивающий объект будет выполняться сначала, за которым следует ограничивающий объект, зависящий от конечной точки, если он существует. Создает
GlobalLimiter
секцию для каждого IPAddress.
- Объект,
// Preceding code removed for brevity.
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var userPolicyName = "user";
var helloPolicy = "hello";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(limiterOptions =>
{
limiterOptions.OnRejected = (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
.CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
.LogWarning("OnRejected: {GetUserEndPoint}", GetUserEndPoint(context.HttpContext));
return new ValueTask();
};
limiterOptions.AddPolicy<string, SampleRateLimiterPolicy>(helloPolicy);
limiterOptions.AddPolicy(userPolicyName, context =>
{
var username = "anonymous user";
if (context.User.Identity?.IsAuthenticated is true)
{
username = context.User.ToString()!;
}
return RateLimitPartition.GetSlidingWindowLimiter(username,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = myOptions.PermitLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
Window = TimeSpan.FromSeconds(myOptions.Window),
SegmentsPerWindow = myOptions.SegmentsPerWindow
});
});
limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, IPAddress>(context =>
{
IPAddress? remoteIpAddress = context.Connection.RemoteIpAddress;
if (!IPAddress.IsLoopback(remoteIpAddress!))
{
return RateLimitPartition.GetTokenBucketLimiter
(remoteIpAddress!, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}
return RateLimitPartition.GetNoLimiter(IPAddress.Loopback);
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages().RequireRateLimiting(userPolicyName);
app.MapDefaultControllerRoute();
static string GetUserEndPoint(HttpContext context) =>
$"User {context.User.Identity?.Name ?? "Anonymous"} endpoint:{context.Request.Path}"
+ $" {context.Connection.RemoteIpAddress}";
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
.RequireRateLimiting(userPolicyName);
app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
.RequireRateLimiting(helloPolicy);
app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}");
app.Run();
Предупреждение
Создание секций на IP-адресах клиента делает приложение уязвимым к атакам типа "отказ в обслуживании", которые используют спуфинго IP-адрес. Дополнительные сведения см. в статье BCP 38 RFC 2827 Network Ingress Filtering: поражение атак типа "отказ в обслуживании", использующих подпуфинирование IP-адресов.
Полный файл см . в репозитории Program.cs
примеров.
Класс 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
});
}
}
В приведенном выше коде OnRejected используется OnRejectedContext для задания состояния ответа 429 слишком много запросов. Состояние отклонений по умолчанию — 503 служба недоступна.
Ограничение с авторизацией
В следующем примере используются веб-маркеры JSON (JWT) и создается секция с маркером доступа JWT. В рабочем приложении JWT обычно предоставляется сервером, выполняющим роль службы маркеров безопасности (STS). Для локальной разработки средство командной строки dotnet user-jwts можно использовать для создания локальных JWTs для конкретного приложения и управления ими.
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}";
Ограничение с ConcurrencyLimiter
параметром , TokenBucketRateLimiter
а также авторизацией
Следующий пример:
ConcurrencyLimiter
Добавляет имя политики, используемое"get"
на Razor страницах.TokenBucketRateLimiter
Добавляет раздел для каждого авторизованного пользователя и секции для всех анонимных пользователей.- Задает RateLimiterOptions.RejectionStatusCode значение 429 слишком много запросов.
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
});
}));
Полный файл см . в репозитории Program.cs
примеров.
Тестирование конечных точек с ограничением скорости
Прежде чем развертывать приложение с использованием ограничения скорости в рабочей среде, протестируйте приложение для проверки ограничений скорости и используемых параметров. Например, создайте сценарий JMeter с помощью средства, например BlazeMeter или Apache JMeter HTTP(S) Test Script Recorder и загрузите скрипт в Azure Load Testing.
Создание секций с вводом пользователем делает приложение уязвимым к атакам типа "отказ в обслуживании " (DoS). Например, создание секций на IP-адресах клиента делает приложение уязвимым к атакам типа "отказ в обслуживании", которые используют подпуфинирование IP-адресов. Дополнительные сведения см. в статье BCP 38 RFC 2827 Network Ingress Filtering: поражение атак типа "отказ в обслуживании", использующих подпуфинирование IP-адресов.
Дополнительные ресурсы
- Ограничение скорости по промежуточного слоя Maarten Balliauw обеспечивает отличное введение и обзор ограничения скорости.
- Ограничение скорости обработчика HTTP в .NET
ASP.NET Core