Ograniczanie szybkości za pomocą middleware w ASP.NET Core

Przez Arvin Kahbazi, Maarten Balliauw i Rick Anderson

Oprogramowanie Microsoft.AspNetCore.RateLimiting pośredniczące zapewnia ograniczanie szybkości. Aplikacje konfigurują zasady ograniczania szybkości, a następnie dołączają zasady do punktów końcowych. Aplikacje korzystające z ograniczania szybkości powinny być dokładnie testowane i sprawdzane przed wdrożeniem. Aby uzyskać więcej informacji, zobacz Testowanie punktów końcowych z ograniczaniem szybkości w tym artykule.

Aby zapoznać się z wprowadzeniem do ograniczania szybkości, zajrzyj do Ograniczania szybkości middleware.

Dlaczego warto używać ograniczania szybkości

Ograniczanie szybkości może służyć do zarządzania przepływem żądań przychodzących do aplikacji. Najważniejsze powody implementacji ograniczania szybkości:

  • Zapobieganie nadużyciom: ograniczanie szybkości pomaga chronić aplikację przed nadużyciami, ograniczając liczbę żądań, które użytkownik lub klient może wykonać w danym okresie. Ta ochrona jest szczególnie ważna w przypadku publicznych interfejsów API.

  • Zapewnianie sprawiedliwego użycia: ustawiając limity uniemożliwiające użytkownikom monopolizację systemu, upewnij się, że wszyscy użytkownicy mają sprawiedliwy dostęp do zasobów.

  • Ochrona zasobów: ograniczanie szybkości pomaga zapobiec przeciążeniu serwera, kontrolując liczbę żądań, które można przetworzyć. Chroni zasoby zaplecza przed przeciążeniami.

  • Zwiększenie bezpieczeństwa: może ograniczyć ryzyko ataków typu "odmowa usługi" (DoS), ograniczając szybkość przetwarzania żądań. Utrudnia to atakującym przeciążenie systemu.

  • Poprawa wydajności: kontrolując szybkość żądań przychodzących, można zachować optymalną wydajność i czas odpowiedzi aplikacji, zapewniając lepsze środowisko użytkownika.

  • Cost Management: w przypadku usług, które generują koszty na podstawie użycia, ograniczanie liczby żądań może pomóc w zarządzaniu i przewidywaniu wydatków przez kontrolowanie ilości przetworzonych żądań.

Implementowanie ograniczania szybkości w aplikacji ASP.NET Core może pomóc w utrzymaniu stabilności, bezpieczeństwa i wydajności. Wynikiem jest niezawodna i wydajna usługa dla wszystkich użytkowników.

Zapobieganie atakom DDoS

Chociaż ograniczanie szybkości może pomóc w ograniczeniu ryzyka ataków typu "odmowa usługi" (DoS), ograniczając szybkość przetwarzania żądań, nie jest to kompleksowe rozwiązanie dla ataków DDoS (Distributed Denial of Service). Ataki DDoS obejmują wiele systemów przytłaczających aplikację powodzią żądań, co sprawia, że samo ograniczanie szybkości staje się niewystarczające.

Aby uzyskać niezawodną ochronę przed atakami DDoS, rozważ użycie komercyjnej usługi ochrony przed atakami DDoS. Te usługi oferują zaawansowane funkcje, takie jak:

  • Analiza ruchu: ciągłe monitorowanie i analiza ruchu przychodzącego w celu wykrywania i eliminowania ataków DDoS w czasie rzeczywistym.
  • skalowalność: możliwość obsługi ataków na dużą skalę przez dystrybucję ruchu między wieloma serwerami i centrami danych.
  • Zautomatyzowane środki zaradcze: zautomatyzowane mechanizmy reagowania w celu szybkiego blokowania złośliwego ruchu bez ręcznej interwencji.
  • Sieć globalna: globalna sieć serwerów do absorbowania i eliminowania ataków bliżej źródła.
  • Stałe aktualizacje: usługi komercyjne stale śledzą i aktualizują swoje mechanizmy ochrony, aby dostosować się do nowych i zmieniających się zagrożeń.

W przypadku korzystania z usługi hostingu w chmurze ochrona przed atakami DDoS jest zwykle dostępna w ramach rozwiązania hostingu, takiego jak Azure Web Application Firewall, AWS Shield lub Google Cloud Armor. Dedykowane zabezpieczenia są dostępne jako zapory aplikacji internetowej (WAF) lub w ramach rozwiązania CDN, takiego jak Cloudflare lub Akamai Kona Site Defender

Zaimplementowanie komercyjnej usługi ochrony przed atakami DDoS w połączeniu z ograniczaniem szybkości może zapewnić kompleksową strategię obrony, zapewniając stabilność, bezpieczeństwo i wydajność aplikacji.

Korzystanie z oprogramowania pośredniczącego ograniczającego szybkość

W poniższych krokach pokazano, jak używać oprogramowania pośredniczącego ograniczającego szybkość w aplikacji ASP.NET Core:

  1. Konfigurowanie usług ograniczania szybkości.

W pliku Program.cs skonfiguruj usługi ograniczające szybkość, dodając odpowiednie zasady ograniczania szybkości. Zdefiniuj zasady jako zasady globalne lub nazwane. Poniższy przykład zezwala na 10 żądań na minutę według użytkownika (tożsamości) lub globalnie:

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)
            }));
});

Nazwane zasady muszą być jawnie stosowane do stron lub punktów końcowych. Poniższy przykład dodaje zasadę ograniczania szybkości w stałym oknie o nazwie "fixed", którą później dodasz do punktu końcowego:

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();

Globalny limiter ma zastosowanie do wszystkich punktów końcowych automatycznie podczas konfigurowania go za pośrednictwem opcji. GlobalLimiter.

  1. Włączanie oprogramowania pośredniczącego ograniczającego szybkość

    W pliku Program.cs włącz oprogramowanie pośredniczące ograniczające szybkość, wywołując UseRateLimiter:

app.UseRouting();

app.UseRateLimiter();

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

app.Run();

Zastosuj zasady ograniczania szybkości do punktów końcowych lub stron

Stosowanie ograniczania szybkości do punktów końcowych internetowego interfejsu API

Zastosuj nazwane zasady do punktu końcowego lub grupy, na przykład:


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

Stosowanie ograniczania szybkości do kontrolerów MVC

Zastosuj skonfigurowane zasady ograniczania szybkości do określonych punktów końcowych lub globalnie. Aby na przykład zastosować politykę "stałą" do wszystkich punktów końcowych kontrolera:

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

Stosowanie ograniczania szybkości do aplikacji Blazor po stronie serwera

Aby ustawić ograniczanie szybkości dla wszystkich routowalnych składników Razor aplikacji, określ RequireRateLimiting z nazwą zasad ograniczania szybkości w wywołaniu MapRazorComponents w pliku Program. W poniższym przykładzie zastosowano zasady ograniczania szybkości o nazwie "policy":

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

Aby ustawić zasadę dla pojedynczego składnika z możliwością routingu Razor lub folderu składników za pośrednictwem pliku importów (_Imports.razor), zastosuj atrybut [EnableRateLimiting] z nazwą zasady. W poniższym przykładzie zastosowano zasadę ograniczania szybkości o nazwie "override". Polityka zastępuje wszelkie polityki, które są obecnie stosowane na tym punkcie końcowym. Globalny limiter nadal działa w punkcie dostępu z zastosowaniem tego atrybutu.

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

<h1>Counter</h1>

Atrybut [EnableRateLimiting] stosuj tylko do komponentu routowalnego lub folderu komponentów za pośrednictwem pliku importów, jeśli RequireRateLimitingnie jest wywoływane dla MapRazorComponents.

Użyj [DisableRateLimiting]atrybutu, aby wyłączyć ograniczanie szybkości dla komponentu z routingiem lub folderu komponentów za pośrednictwem pliku importów.

Algorytmy ogranicznika szybkości

Klasa RateLimiterOptionsExtensions udostępnia następujące metody rozszerzenia na potrzeby ograniczania szybkości:

Stałe, przesuwane i limitatory tokenów ograniczają maksymalną liczbę żądań w danym okresie. Ogranicznik współbieżności ogranicza tylko liczbę współbieżnych żądań i nie ogranicza liczby żądań w danym okresie. Podczas wybierania ogranicznika należy wziąć pod uwagę koszt punktu końcowego. Koszt punktu końcowego obejmuje używane zasoby, takie jak czas, dostęp do danych, procesor CPU i operacje we/wy.

Stały ogranicznik okien

Metoda AddFixedWindowLimiter używa stałego przedziału czasu w celu ograniczenia żądań. Gdy okres upłynie, rozpoczyna się nowy okres, a limit żądań zostaje zresetowany.

Spójrzmy na poniższy kod:

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();

Poniższy kod:

  • Wywołuje AddRateLimiter w celu dodania usługi ograniczania szybkości do kolekcji usług.
  • Wywołania AddFixedWindowLimiter w celu utworzenia stałego limitatora okien z nazwą polityki "fixed" i ustawieniami:
  • PermitLimit do 4 i czas Window do 12. Dozwolone jest maksymalnie 4 żądania na każde 12-sekundowe okno.
  • QueueProcessingOrder do OldestFirst.
  • QueueLimit na 2 (ustaw tę wartość na 0, aby wyłączyć mechanizm kolejkowania).
  • Wywołuje metodę UseRateLimiter , aby włączyć ograniczanie szybkości.

Aplikacje powinny używać konfiguracji do ustawiania opcji ogranicznika. Poniższy kod aktualizuje powyższy kod przy użyciu polecenia MyRateLimitOptions na potrzeby konfiguracji:

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 należy wywołać po UseRouting, kiedy używane są interfejsy API specyficzne dla punktu końcowego do limitowania szybkości. Na przykład, jeśli używany jest atrybut [EnableRateLimiting], UseRateLimiter musi zostać wywołane po UseRouting. Podczas wywoływania tylko globalnych ograniczników, można wywołać UseRateLimiter przed UseRouting.

Ogranicznik okna przesuwanego

Algorytm okna przesuwanego:

  • Jest podobny do stałego limitatora okien, ale dodaje segmenty na okno. Okno przesuwa się o jeden segment w każdym interwale segmentów. Interwał segmentu to (czas okna)/(segmenty na okno).
  • Ogranicza żądania dla okna do permitLimit żądań.
  • Każde okno czasowe jest podzielone na n segmenty na okno.
  • Żądania pobrane z wygasłego segmentu czasowego o jedno okno wstecz (n segmenty sprzed bieżącego segmentu) są dodawane do bieżącego segmentu. Odnosimy się do najbardziej przeterminowanego segmentu czasu z jedno okno wstecz jako segmentu przeterminowanego.

Rozważmy poniższą tabelę, która przedstawia przesuwany ogranicznik okna z 30-sekundowym oknem, trzema segmentami na okno i limitem 100 żądań:

  • Górny wiersz i pierwsza kolumna zawierają segment czasu.
  • Drugi wiersz zawiera pozostałe dostępne żądania. Pozostałe żądania są obliczane jako dostępne żądania pomniejszone o przetworzone żądania oraz żądania z recyklingu.
  • Żądania każdorazowo przesuwają się wzdłuż ukośnej niebieskiej linii.
  • Od czasu 30 żądania pobrane z wygasłego przedziału czasowego są dodawane z powrotem do limitu żądań, jak pokazano na czerwonych liniach.

Tabela przedstawiająca żądania, limity i miejsca recyklingu

W poniższej tabeli przedstawiono dane w poprzednim grafie w innym formacie. W kolumnie Dostępne są wyświetlane żądania dostępne z poprzedniego segmentu (Przeniesienie z poprzedniego wiersza). Pierwszy wiersz zawiera 100 dostępnych żądań, ponieważ nie ma poprzedniego segmentu.

Czas Dostępny Zajęte Odzyskane z przeterminowanych produktów Przewoż
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

W poniższym kodzie jest używany ogranicznik szybkości okien przesuwnych:

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();

Ogranicznik zasobnika tokenu

Ogranicznik zasobnika tokenów jest podobny do ogranicznika okna przesuwanego, ale zamiast ponownego dodawania żądań pobranych z wygasłego segmentu, w każdym okresie uzupełniania dodawana jest stała liczba tokenów. Tokeny dodane przez poszczególne segmenty nie mogą zwiększyć dostępnych tokenów do liczby wyższej niż limit zasobnika tokenu. W poniższej tabeli przedstawiono limit zasobnika tokenu z limitem 100 tokenów i 10-sekundowym okresem uzupełniania.

Czas Dostępny Zajęte Dodane Przewoż
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

Poniższy kod używa ogranicznika zasobnika tokenu:

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();

Gdy AutoReplenishment jest ustawione na true, wewnętrzny czasomierz uzupełnia tokeny co ReplenishmentPeriod; gdy jest ustawione na false, aplikacja musi wywołać TryReplenish na limiterze.

Ogranicznik współbieżności

Ogranicznik współbieżności ogranicza liczbę współbieżnych żądań. Każde żądanie zmniejsza limit współbieżności o jeden. Po zakończeniu żądania limit zostanie zwiększony o jeden. W przeciwieństwie do innych ograniczników żądań, które ograniczają łączną liczbę żądań dla określonego okresu, limitator współbieżności ogranicza tylko liczbę współbieżnych żądań i nie ogranicza liczby żądań w danym okresie.

Poniższy kod używa ogranicznika współbieżności:

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();

Ograniczanie szybkości partycji

Partycje ograniczania szybkości dzielą ruch na oddzielne segmenty, z których każdy ma własne liczniki limitów. Takie podejście zapewnia bardziej szczegółową kontrolę niż pojedynczy licznik globalny. Różne klucze, takie jak identyfikator użytkownika, adres IP lub klucz interfejsu API, definiują zasobniki partycji.

Zalety partycjonowania

  • Sprawiedliwość: jeden użytkownik nie może korzystać z całego limitu szybkości dla wszystkich użytkowników.
  • Stopień szczegółowości: różne limity dla różnych użytkowników i zasobów.
  • Bezpieczeństwo: Lepsza ochrona przed ukierunkowanym nadużyciem.
  • Usługa warstwowa: obsługa warstw usług z różnymi limitami.

Ograniczanie szybkości partycjonowanej zapewnia szczegółową kontrolę nad sposobem zarządzania ruchem interfejsu API przy jednoczesnym zapewnieniu sprawiedliwej alokacji zasobów.

Według adresu 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)
        }));

Według tożsamości użytkownika

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

Według klucza 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)
            }),
    };
});

Według ścieżki punktu końcowego

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)
        });
});

Tworzenie ograniczników łańcuchowych

Interfejs API CreateChained przyjmuje wiele wystąpień PartitionedRateLimiter i łączy je w jedno PartitionedRateLimiter. Połączony ogranicznik uruchamia wszystkie limitery wejściowe w sekwencji. Ponieważ wynikiem jest PartitionedRateLimiter przypisane do GlobalLimiter, łańcuch dotyczy każdego punktu końcowego. Aby zamiast tego utworzyć łańcuch ograniczników dla określonego punktu końcowego, użyj nazwanych zasad, jak pokazano w temacie Ograniczniki łańcucha w nazwanych zasadach.

Poniższy kod używa metody 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();

Aby uzyskać więcej informacji, zobacz kod źródłowy CreateChained.

Ograniczniki łańcucha w nazwanych zasadach

CreateChained łańcuchy globalnych ograniczników, które mają zastosowanie do każdego punktu końcowego. Aby połączyć wiele typów ograniczników i ograniczyć ich zakres do określonych punktów końcowych, połącz limitery wewnątrz nazwanych zasad za pomocą polecenia CreateChained. To przeciążenie zwraca pojedynczy element RateLimiter, który uruchamia każde ograniczenie sekwencyjnie, co odpowiada typowi zwracanemu wymaganemu przez fabrykę partycji nazwanej polityki.

Poniższa zasada "combined" łączy ogranicznik typu token bucket i ogranicznik współbieżności za pomocą RateLimiter.CreateChained, a następnie stosuje tę zasadę do pojedynczego punktu końcowego za pomocą 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");

Żądanie musi uzyskać dzierżawę od każdego ogranicznika w łańcuchu, aby kontynuować przetwarzanie, a ograniczniki są wykonywane w kolejności, w jakiej zostały przekazane do RateLimiter.CreateChained. Jeśli limiter odrzuci żądanie, żądanie zostanie odrzucone, a dzierżawy już uzyskane z wcześniejszych ograniczników w łańcuchu zostaną usunięte w odwrotnej kolejności.

Podczas tworzenia łańcuchów ograniczników w nazwanych zasadach należy pamiętać o następujących kwestiach:

  • Usuwanie dzierżawy zwraca zezwolenie na ograniczenie współbieżności. Ograniczniki oparte na czasie (wiadro tokenów, stałe okno i okno przesuwne) nie zwracają pozwolenia, które zostało już przyznane, gdy późniejszy ogranicznik w łańcuchu odrzuci żądanie, więc należy wziąć to pod uwagę przy ustalaniu kolejności ograniczników w łańcuchu.
  • Bezpośrednie skonstruowanie obiektu TokenBucketRateLimiter z ustawieniem AutoReplenishment na true powoduje, że każde wystąpienie ogranicznika ma własny timer. Funkcje pomocnicze AddTokenBucketLimiter i RateLimitPartition.GetTokenBucketLimiter zamiast tego ustawiają false na AutoReplenishment i odnawiają wszystkie swoje ograniczniki za pomocą jednego współdzielonego czasomierza.
  • RateLimiter.CreateChained nie usuwa przekazanych do niego ograniczników. W poprzednim przykładzie partycja buforuje łańcuchowy ogranicznik, a struktura zarządza okresem istnienia. W przypadku tworzenia ograniczników łańcuchowych poza partycją należy usunąć wewnętrzne ograniczniki, gdy nie są już używane.
  • Preferuj podejście globalne PartitionedRateLimiter.CreateChained , gdy łańcuch powinien mieć zastosowanie do każdego punktu końcowego. Używaj nazwanej zasady z RateLimiter.CreateChained tylko wtedy, gdy łańcuch musi być ograniczony do określonych punktów końcowych.

Wybieranie tego, co się stanie, gdy żądanie jest ograniczone limitem

W prostych przypadkach można po prostu ustawić kod stanu:

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

    // Rate limiter configuration...
});

Najczęstszym podejściem jest zarejestrowanie wywołania zwrotnego OnRejected podczas konfigurowania ograniczania szybkości:

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

    options.OnRejected = async (context, cancellationToken) =>
    {
        // Custom rejection handling logic
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.HttpContext.Response.Headers["Retry-After"] = "60";

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

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

Inną opcją jest ustawienie żądania w kolejce:

Kolejkowanie żądań

Po włączeniu kolejkowania, jeśli żądanie przekroczy limit szybkości, system umieszcza go w kolejce. Żądanie czeka w kolejce, aż pozwolenie stanie się dostępne lub wystąpi limit czasu. System przetwarza żądania zgodnie z konfigurowalną kolejnością w kolejce.

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
    });
});

atrybuty EnableRateLimiting i DisableRateLimiting

Zastosuj atrybuty [EnableRateLimiting] i [DisableRateLimiting] do kontrolera, metody akcji lub strony Razor. W przypadku stron Razor zastosuj atrybut do strony Razor, a nie do modułów obsługi strony. Na przykład nie można zastosować [EnableRateLimiting] do OnGet, OnPost ani żadnego innego programu obsługi strony.

Atrybut [DisableRateLimiting]wyłącza ograniczanie szybkości dla kontrolera, metody akcji lub Razor strony, niezależnie od nazwanych ograniczników szybkości lub globalnych ograniczników. Rozważmy na przykład następujący kod, który wywołuje RequireRateLimiting w celu zastosowania ograniczenia szybkości do wszystkich punktów końcowych kontrolera:

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();

W poniższym kodzie [DisableRateLimiting] wyłącza ograniczanie szybkości i przesłania [EnableRateLimiting("fixed")] stosowane do Home2Controller i app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) wywoływanego w 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 });
    }
}

W poprzednim kodzie element [EnableRateLimiting("sliding")] nie jest stosowany do metody akcji , ponieważ wywołano Privacy.

Rozważ następujący kod, który nie wywołuje RequireRateLimiting na MapRazorPages lub 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();

Rozważmy następujący kontroler:

[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 });
    }
}

W poprzednim kontrolerze:

  • Ogranicznik szybkości zasad "fixed" jest stosowany do wszystkich metod działania, które nie mają atrybutów EnableRateLimiting lub DisableRateLimiting.
  • Ogranicznik stawki polityki jest stosowany do działania "sliding".
  • Ograniczanie szybkości jest wyłączone w metodzie NoLimit akcji.

Metryki ograniczania szybkości

Oprogramowanie pośredniczące ograniczające szybkość zapewnia wbudowane metryki i funkcje monitorowania , które ułatwiają zrozumienie, w jaki sposób limity szybkości wpływają na wydajność aplikacji i środowisko użytkownika. Aby uzyskać listę metryk, zobacz Microsoft.AspNetCore.RateLimiting.

Testowanie punktów końcowych z ograniczaniem szybkości

Przed wdrożeniem aplikacji korzystającej z ograniczania szybkości w środowisku produkcyjnym przetestuj aplikację w celu zweryfikowania użytych ograniczników szybkości i opcji. Na przykład utwórz skrypt JMeter przy użyciu narzędzia takiego jak BlazeMeter lub Apache JMeter HTTP(S) Test Script Recorder i załaduj skrypt do Azure Load Testing.

Jeśli tworzysz partycje przy użyciu danych wejściowych użytkownika, aplikacja staje się podatna na ataki typu "odmowa usługi " (DoS). Jeśli na przykład tworzysz partycje przy użyciu adresów IP klienta, aplikacja staje się podatna na ataki typu "odmowa usługi", które używają fałszowania adresów źródłowych IP. Aby uzyskać więcej informacji, zobacz Filtrowanie ruchu przychodzącego sieci BCP 38 RFC 2827: pokonanie ataków typu "odmowa usługi", które korzystają z fałszowania adresów źródłowych IP.

Dodatkowe zasoby