ПО промежуточного слоя для ограничения скорости в ASP.NET Core

Арвин Кахбази, Маартен Бальяув, и Рик Андерсон

ПО промежуточного слоя Microsoft.AspNetCore.RateLimiting предоставляет функцию ограничения скорости. Приложения настраивают политики ограничения скорости, а затем присоединяют политики к конечным точкам. Приложения, использующие ограничение скорости, должны тщательно тестироваться и проверяться перед развертыванием. Дополнительные сведения см. в разделе "Тестирование конечных точек с ограничением скорости " в этой статье.

Для ознакомления с ограничением скорости см. раздел "Ограничение скорости в посредническом слое".

Почему используйте ограничение скорости

Ограничение скорости можно использовать для управления потоком входящих запросов к приложению. Основные причины реализации ограничения скорости:

  • предотвращение злоупотреблений: ограничение скорости помогает защитить приложение от злоупотреблений, ограничив количество запросов, которые пользователь или клиент могут выполнять в течение заданного периода времени. Эта защита особенно важна для общедоступных API.
  • Обеспечение справедливого использования: задав ограничения, которые препятствуют монополизировать систему пользователям, убедитесь, что у всех пользователей есть справедливый доступ к ресурсам.
  • Защита ресурсов: ограничение скорости помогает предотвратить перегрузку сервера, контролируя количество запросов, которые можно обрабатывать. Он защищает внутренние ресурсы от перегрузки.
  • Повышение безопасности. Это может снизить риск атак типа "отказ в обслуживании" (DoS), ограничив скорость обработки запросов. Это затрудняет злоумышленникам перегрузку системы потоком запросов.
  • Повышение производительности. Управляя скоростью входящих запросов, вы можете поддерживать оптимальную производительность и скорость реагирования приложения, обеспечивая более эффективное взаимодействие с пользователем.
  • Управление затратами. Для служб, которые несут затраты на основе использования, ограничение скорости может помочь контролировать и прогнозировать расходы, управляя объемом обработанных запросов.

Реализация ограничения скорости в приложении ASP.NET Core может помочь обеспечить стабильность, безопасность и производительность. Результатом является надежная и эффективная служба для всех пользователей.

Предотвращение атак DDoS

Хотя ограничение скорости может помочь снизить риск атак типа "отказ в обслуживании" (DoS), ограничив скорость обработки запросов, это не комплексное решение для атак типа "отказ в обслуживании" (DDoS). Атаки DDoS предполагают использование нескольких систем, которые перегружают приложение потоком запросов, что делает трудным управление одной только помощью ограничения скорости.

Для надежной защиты от атак DDoS рекомендуется использовать коммерческую службу защиты от атак DDoS. Эти службы предлагают дополнительные функции, такие как:

  • Анализ трафика: непрерывный мониторинг и анализ входящего трафика для обнаружения и устранения атак DDoS в режиме реального времени.
  • масштабируемость. Возможность обработки крупномасштабных атак путем распределения трафика между несколькими серверами и центрами обработки данных.
  • Автоматическое устранение рисков: автоматизированные механизмы реагирования для быстрого блокирования вредоносного трафика без вмешательства вручную.
  • Глобальная сеть: глобальная сеть серверов для поглощения и устранения атак ближе к источнику.
  • Постоянные обновления: коммерческие службы постоянно отслеживают и обновляют свои механизмы защиты, чтобы адаптироваться к новым и изменяющимся угрозам.

При использовании облачной службы размещения защита от атак DDoS обычно доступна как часть решения для размещения, например брандмауэра веб-приложений Azure, AWS Shield или Google Cloud Armor. Выделенные средства защиты доступны как брандмауэры веб-приложений (WAF) или как часть решения CDN, например Cloudflare или Akamai Kona Site Defender

Реализация коммерческой службы защиты от атак DDoS в сочетании с ограничением скорости может обеспечить комплексную стратегию защиты, обеспечивая стабильность, безопасность и производительность приложения.

Используйте промежуточное ПО для ограничения частоты запросов

Ниже показано, как использовать ПО промежуточного слоя с ограничением скорости в приложении ASP.NET Core:

  1. Настройка служб ограничения скорости.

В файле Program.cs настройте службы ограничения скорости, добавив соответствующие политики ограничения скорости. Определите политики как глобальные или именованные политики. В следующем примере допускается 10 запросов в минуту по пользователю (удостоверению) или по всему миру:

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

Именованные политики необходимо явно применять к страницам или конечным точкам. В следующем примере добавляется политика ограничения скорости с фиксированным окном с именем "fixed", которую позже добавляют к конечной точке:

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

Глобальный предел применяется ко всем конечным точкам автоматически при настройке с помощью параметров. GlobalLimiter.

  1. Включите посредствующий слой ограничения скорости

    В файле Program.cs включите промежуточное ПО для ограничения скорости, вызвав UseRateLimiter:

app.UseRouting();

app.UseRateLimiter();

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

app.Run();

Применение политик ограничения скорости к конечным точкам или страницам

Применение ограничения скорости к конечным точкам веб-API

Примените именованную политику к конечной точке или группе, например:


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

Применение ограничения скорости к контроллерам MVC

Примените настроенные политики ограничения скорости к определенным конечным точкам или глобально. Например, чтобы применить политику "fixed" ко всем конечным точкам контроллера:

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

Применение ограничения скорости к приложениям на стороне сервера Blazor

Чтобы задать ограничение скорости для всех маршрутизируемых Razor компонентов приложения, укажите RequireRateLimiting с именем политики ограничения скорости в вызове MapRazorComponents в файле Program. В следующем примере применяется политика ограничения скорости с именем "policy".

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

Чтобы задать политику для одного routable Razor компонента или папки компонентов с помощью файла импорта (_Imports.razor), примените [EnableRateLimiting] атрибут с именем политики. В следующем примере применяется политика ограничения скорости с именем "override". Политика заменяет все политики, применяемые к конечной точке. Глобальный ограничитель по-прежнему работает на конечной точке с применением этого атрибута.

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

<h1>Counter</h1>

[EnableRateLimiting] Примените атрибут только к маршрутизируемому компоненту или папке компонентов через файл импорта, если RequireRateLimitingон не вызываетсяMapRazorComponents.

Используйте атрибут [DisableRateLimiting] attribute, чтобы отключить ограничение частоты запросов для маршрутизируемого компонента или папки компонентов с помощью файла imports.

Алгоритмы ограничения скорости

Класс 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 (задайте для этого значение 0, чтобы отключить механизм очереди).
  • Вызовы 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
шестьдесят 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 девяносто
20 девяносто 5 15 100
30 100 30 20 девяносто
40 девяносто 6 16 100
50 100 40 20 80
шестьдесят 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();

Ограничения скорости секций

Разделы для ограничения скорости разделяют трафик на отдельные группы, каждая из которых получает собственные счетчики лимита. Этот подход обеспечивает более детализированный контроль, чем один глобальный счетчик. Различные ключи, такие как идентификатор пользователя, IP-адрес или ключ API, определяют контейнеры секций.

Преимущества секционирования

  • Справедливость: один пользователь не может использовать весь предел скорости для всех.
  • Степень детализации: различные ограничения для разных пользователей и ресурсов.
  • Безопасность: улучшенная защита от целевых злоупотреблений.
  • Многоуровневая служба: поддержка уровней служб с различными ограничениями.

Ограничение секционированных скоростей позволяет точно контролировать управление трафиком API, обеспечивая справедливое распределение ресурсов.

По IP-адресу

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

По удостоверению пользователя

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

По ключу API

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

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

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

По пути эндпоинта

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

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

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

Создайте цепные ограничители

CreateChained API принимает несколько PartitionedRateLimiter экземпляров и объединяет их в одинPartitionedRateLimiter. Объединенный ограничитель выполняет все входные ограничения в последовательности. Поскольку результат — это GlobalLimiter, назначенный PartitionedRateLimiter, цепочка применяется ко всем конечным точкам. Чтобы вместо этого объединить ограничители в цепочку для конкретной конечной точки, используйте именованную политику, как показано в разделе Объединение ограничителей в цепочку в именованной политике.

В приведенном ниже коде используется 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();

Дополнительные сведения см. в исходном коде CreateChained.

Ограничители цепочки в именованной политике

CreateChained объединяет в цепочку глобальные ограничители, применяемые к каждой конечной точке. Чтобы объединить несколько типов ограничителей и применить их к определенным конечным точкам, объедините ограничители в цепочку внутри именованной политики с помощью CreateChained. Эта перегрузка возвращает один объект RateLimiter, который последовательно запускает каждый ограничитель и требуется в качестве возвращаемого типа для фабрики секций именованной политики.

Следующая "combined" политика объединяет ограничивающий контейнер маркеров и ограничение параллелизма с RateLimiter.CreateChained, а затем применяет политику к одной конечной точке с RequireRateLimiting:

var builder = WebApplication.CreateBuilder(args);

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

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

var app = builder.Build();

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

Чтобы продолжить выполнение, запрос должен получить разрешение от каждого ограничителя в цепочке, а ограничители обрабатываются в порядке, переданном в RateLimiter.CreateChained. Если какой-либо ограничитель отклоняет запрос, запрос отклоняется, а разрешения, уже полученные от предыдущих ограничителей в цепочке, освобождаются в обратном порядке.

При связывании ограничений в именованной политике следует учитывать следующее:

  • Удаление аренды возвращает разрешение на ограничение параллелизма. Ограничители на основе времени (маркерное ведро, фиксированное окно и скользящее окно) не возвращают уже полученное разрешение, если следующий ограничитель в цепочке отклоняет запрос, поэтому учитывайте это при определении порядка ограничителей в цепочке.
  • Прямое создание AutoReplenishment при значении true, равном TokenBucketRateLimiter, дает каждому экземпляру ограничителя собственный таймер. Вместо этого вспомогательные функции RateLimitPartition.GetTokenBucketLimiter и AddTokenBucketLimiter устанавливают для AutoReplenishment значение false и пополняют все свои ограничители от одного общего таймера.
  • RateLimiter.CreateChained не удаляет ограничения, переданные в него. В предыдущем примере раздел кэширует цепочный ограничитель, а фреймворк управляет его жизненным циклом. Если вы создаёте цепочки ограничителей за пределами раздела, освобождайте внутренние ограничители, когда они больше не нужны.
  • Предпочитайте глобальный PartitionedRateLimiter.CreateChained подход, когда цепочка должна применяться к каждой конечной точке. Используйте именованную политику RateLimiter.CreateChained , только если цепочка должна быть ограничена определенными конечными точками.

Выбор того, что происходит при ограничении скорости запроса

В простых случаях можно просто задать код состояния:

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

    // Rate limiter configuration...
});

Наиболее распространенный подход состоит в том, чтобы зарегистрировать OnRejected-вызов при настройке ограничения скорости:

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

    options.OnRejected = async (context, cancellationToken) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;

        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((long)Math.Ceiling(retryAfter.TotalSeconds)).ToString(NumberFormatInfo.InvariantInfo);
        }

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

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

Для примера требуется using System.Globalization; и using System.Threading.RateLimiting;.

Note

FixedWindowRateLimiter отчеты MetadataName.RetryAfter на основе следующей границы фиксированного окна. Приложения, которые копируют эти метаданные в Retry-After заголовок ответа, OnRejected возвращают точный интервал повторных попыток автоматически. Ограничители, которые не могут оценить, когда доступны пропуска, например ConcurrencyLimiter, не предоставляют метаданные RetryAfter.

Другим вариантом является очередь запроса:

Очередь запросов

При включении очереди, если запрос превышает ограничение скорости, система помещает его в очередь. Запрос ожидает в очереди до тех пор, пока разрешение не станет доступным или не истечёт время ожидания. Система обрабатывает запросы в соответствии с настраиваемым порядком очереди.

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

Атрибуты EnableRateLimiting и DisableRateLimiting

Примените атрибуты [EnableRateLimiting] и [DisableRateLimiting] к контроллеру, методу действия или странице Razor Page. Для 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 и app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy), вызываемым в 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 });
    }
}

В приведенном выше коде [EnableRateLimiting("sliding")]не применяется к методу Privacy действия, потому что вызывается Program.csapp.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 действия.

Ограничение частоты метрик

Промежуточное ПО для ограничения скорости предоставляет встроенные метрики и возможности мониторинга, помогающие понять, как ограничения скорости влияют на производительность приложения и пользовательский опыт. Список метрик см. в разделе Microsoft.AspNetCore.RateLimiting.

Тестирование конечных точек с ограничением скорости

Перед развертыванием приложения, использующего ограничение скорости в рабочей среде, протестируйте приложение для проверки ограничений скорости и используемых параметров. Например, создайте скрипт JMeter с помощью средства, например BlazeMeter или Apache JMeter HTTP(S) Test Script Recorder и загрузите скрипт в Нагрузочное тестирование Azure.

Если вы создаете секции с помощью пользовательских входных данных, ваше приложение становится уязвимым к атакам типа "отказ в обслуживании " (DoS). Например, если вы создаете разделы по IP-адресам клиентов, ваше приложение становится уязвимым для атак типа «отказ в обслуживании», использующих подмену IP-адреса источника. Дополнительные сведения см. в статье BCP 38 RFC 2827 Network Ingress Filtering: защита от атак типа "отказ в обслуживании", использующих спуфинг IP-адресов.