Udostępnij za pośrednictwem


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.

Algorytmy ogranicznika szybkości

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

Stały ogranicznik okien

Metoda AddFixedWindowLimiter używa stałego przedziału czasu w celu ograniczenia żądań. Po wygaśnięciu przedziału czasu zostanie uruchomione nowe okno czasowe i zostanie zresetowany limit żądania.

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łania AddRateLimiter w celu dodania usługi ograniczającej szybkość 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 w n segmentach 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 Podjęte Odzyskany z wygasłego 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 Wzięty 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();

Tworzenie ograniczników łańcuchowych

Interfejs API CreateChained umożliwia przekazywanie wielu PartitionedRateLimiter, które są łączone w jeden element PartitionedRateLimiter. Połączony ogranicznik uruchamia wszystkie limitery wejściowe w sekwencji.

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

EnableRateLimiting i DisableRateLimiting atrybuty

Atrybuty [EnableRateLimiting] i [DisableRateLimiting] można zastosować do kontrolera, metody akcji lub Razor strony. W przypadku Razor stron atrybut musi być stosowany do Razor Strony, a nie do procedur obsługi stron. Na przykład [EnableRateLimiting] nie można zastosować do OnGet, OnPost ani żadnego innego programu obsługi stron.

Atrybut [DisableRateLimiting]wyłącza ograniczanie szybkości 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 Privacy, ponieważ wywołano Program.csapp.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy).

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 Privacy.
  • Ograniczanie szybkości jest wyłączone w metodzie NoLimit akcji.

Stosowanie atrybutów do Razor stron

W przypadku stron Razor atrybut musi być stosowany do strony Razor, a nie do programów obsługi stron. Na przykład [EnableRateLimiting] nie można zastosować do OnGet, OnPost ani żadnego innego obsługującego strony.

Atrybut DisableRateLimiting wyłącza ograniczanie szybkości na Razor stronie. EnableRateLimitingelement jest stosowany tylko do Razor strony, jeśli MapRazorPages().RequireRateLimiting(Policy) nie został wywołany.

Porównanie algorytmów ogranicznika

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. Koszt punktu końcowego należy wziąć pod uwagę podczas wybierania ogranicznika. Koszt punktu końcowego obejmuje używane zasoby, na przykład czas, dostęp do danych, procesor CPU i operacje we/wy.

Przykłady ogranicznika szybkości

Poniższe przykłady nie są przeznaczone dla kodu produkcyjnego, ale są przykładami dotyczącymi używania ograniczników.

Ogranicznik z OnRejected, RetryAfter i GlobalLimiter

Poniższy przykład:

  • Tworzy callback RateLimiterOptions.OnRejected, który jest wywoływany, gdy żądanie przekracza określony limit. retryAfter można używać z TokenBucketRateLimiter, FixedWindowLimiteri SlidingWindowLimiter, ponieważ te algorytmy są w stanie oszacować, kiedy zostanie dodanych więcej zezwoleń. ConcurrencyLimiter nie jest w stanie obliczyć, kiedy zezwolenia będą dostępne.

  • Dodaje następujące ograniczniki:

    • Element SampleRateLimiterPolicy , który implementuje IRateLimiterPolicy<TPartitionKey> interfejs. Klasa SampleRateLimiterPolicy jest przedstawiona w dalszej części tego artykułu.
    • A SlidingWindowLimiter:
      • Z partycją dla każdego uwierzytelnionego użytkownika.
      • Jedna udostępniona partycja dla wszystkich użytkowników anonimowych.
    • Wartość GlobalLimiter , która jest stosowana do wszystkich żądań. Globalny limiter zostanie wykonany najpierw, a następnie ogranicznik specyficzny dla punktu końcowego, jeśli istnieje. Obiekt GlobalLimiter tworzy partycję dla każdego IPAddress.
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
    throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.Configure<MyRateLimitOptions>(
    builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var userPolicyName = "user";
var helloPolicy = "hello";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

builder.Services.AddRateLimiter(limiterOptions =>
{
    limiterOptions.OnRejected = (context, cancellationToken) =>
    {
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
        }

        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
            .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
            .LogWarning("OnRejected: {GetUserEndPoint}", GetUserEndPoint(context.HttpContext));

        return new ValueTask();
    };

    limiterOptions.AddPolicy<string, SampleRateLimiterPolicy>(helloPolicy);
    limiterOptions.AddPolicy(userPolicyName, context =>
    {
        var username = "anonymous user";
        if (context.User.Identity?.IsAuthenticated is true)
        {
            username = context.User.Identity.Name;
        }

        return RateLimitPartition.GetSlidingWindowLimiter(username,
            _ => new SlidingWindowRateLimiterOptions
            {
                PermitLimit = myOptions.PermitLimit,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = myOptions.QueueLimit,
                Window = TimeSpan.FromSeconds(myOptions.Window),
                SegmentsPerWindow = myOptions.SegmentsPerWindow
            });

    });
    
    limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, IPAddress>(context =>
    {
        IPAddress? remoteIpAddress = context.Connection.RemoteIpAddress;

        if (!IPAddress.IsLoopback(remoteIpAddress!))
        {
            return RateLimitPartition.GetTokenBucketLimiter
            (remoteIpAddress!, _ =>
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = myOptions.TokenLimit2,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = myOptions.QueueLimit,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
                    TokensPerPeriod = myOptions.TokensPerPeriod,
                    AutoReplenishment = myOptions.AutoReplenishment
                });
        }

        return RateLimitPartition.GetNoLimiter(IPAddress.Loopback);
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseRateLimiter(); // important to add after UseAuthentication because the limiter uses auth info
app.UseAuthorization();

app.MapRazorPages().RequireRateLimiting(userPolicyName);
app.MapDefaultControllerRoute();

static string GetUserEndPoint(HttpContext context) =>
   $"User {context.User.Identity?.Name ?? "Anonymous"} endpoint:{context.Request.Path}"
   + $" {context.Connection.RemoteIpAddress}";
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
    .RequireRateLimiting(userPolicyName);

app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
    .RequireRateLimiting(helloPolicy);

app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}");

app.Run();

Ostrzeżenie

Tworzenie partycji na adresach IP klienta sprawia, że aplikacja jest podatna na ataki typu "odmowa usługi", które korzystają z 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 wykorzystują fałszowanie adresów źródłowych IP.

Zobacz repozytorium przykładów, aby uzyskać pełny Program.cs plik.

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

W poprzednim kodzie, OnRejected używa OnRejectedContext do ustawienia stanu odpowiedzi na 429 Too Many Requests. Domyślny odrzucony stan to 503 Usługa niedostępna.

Ogranicznik z autoryzacją

W poniższym przykładzie użyto tokenów sieci Web JSON (JWT) i utworzono partycję przy użyciu tokenu dostępu JWT. W aplikacji produkcyjnej zestaw JWT zazwyczaj jest dostarczany przez serwer działający jako usługa tokenu zabezpieczającego (STS). W przypadku programowania lokalnego narzędzie wiersza polecenia dotnet user-jwts może służyć do tworzenia lokalnych zestawów JWTs specyficznych dla aplikacji i zarządzania nimi.

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

Ogranicznik z ConcurrencyLimiter, TokenBucketRateLimiter i autoryzacją

Poniższy przykład:

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

Zobacz repozytorium przykładów, aby uzyskać pełny Program.cs plik.

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

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

Tworzenie partycji z danymi wejściowymi użytkownika sprawia, że aplikacja jest podatna na ataki typu "odmowa usługi " (DoS). Na przykład tworzenie partycji na adresach IP klienta sprawia, że aplikacja jest podatna na ataki typu "odmowa usługi", które korzystają z 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