Ograniczanie szybkości oprogramowania pośredniczącego w programie ASP.NET Core

Przez Arvin Kahbazi, Maarten Balliauw i Rick Anderson

Oprogramowanie Microsoft.AspNetCore.RateLimiting pośredniczące zapewnia ograniczanie szybkości oprogramowania pośredniczącego. 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, zobacz Ograniczanie szybkości oprogramowania pośredniczącego.

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

Powyższy kod ma następujące działanie:

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 użyciu interfejsów API specyficznych dla punktu końcowego ograniczania szybkości. Jeśli na przykład jest używany atrybut, UseRateLimiter należy wywołać metodę [EnableRateLimiting] po UseRouting. Podczas wywoływania tylko globalnych ograniczników UseRateLimiter można wywołać metodę przed UseRouting.

Ogranicznik okna przesuwanego

Algorytm okna przesuwanego:

  • Jest podobny do stałego limitatora okien, ale dodaje segmenty na okno. Okno przesuwa jeden segment każdego interwału segmentu. Interwał segmentu to (czas okna)/(segmenty na okno).
  • Ogranicza żądania dotyczące okna do permitLimit żądań.
  • Każde okno czasowe jest podzielone w n segmentach na okno.
  • Żądania pobrane z wygasłego segmentu czasu z powrotem (n segmenty przed bieżącym segmentem) są dodawane do bieżącego segmentu. Odwołujemy się do najbardziej wygasłego segmentu czasu z powrotem jako wygasłego segmentu.

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 w każdym momencie są przesuwane wzdłuż ukośnej niebieskiej linii.
  • Od czasu 30 na żądanie pobrane z wygasłego segmentu czasu jest dodawane z powrotem do limitu żądań, jak pokazano w czerwonych wierszach.

Table showing requests, limits, and recycled slots

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 ( Przenoszenie 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

Limiter zasobników tokenu jest podobny do limitatora okien przesuwanych, ale zamiast dodawania z powrotem żądań pobranych z wygasłego segmentu, jest dodawana stała liczba tokenów każdego okresu uzupełniania. 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 Podję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 ustawiona wartość true, wewnętrzny czasomierz uzupełnia tokeny co ReplenishmentPeriod; po ustawieniu falsena wartość aplikacja musi wywołać TryReplenish limitator.

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 CreateChained API umożliwia przekazywanie wielu PartitionedRateLimiter , które są łączone w jeden PartitionedRateLimiterelement . 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 = (context, _) =>
    {
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
        }

        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");

        return new ValueTask();
    };
    _.GlobalLimiter = PartitionedRateLimiter.CreateChained(
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();

            return RateLimitPartition.GetFixedWindowLimiter
            (userAgent, _ =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 4,
                    Window = TimeSpan.FromSeconds(2)
                });
        }),
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();
            
            return RateLimitPartition.GetFixedWindowLimiter
            (userAgent, _ =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 20,    
                    Window = TimeSpan.FromSeconds(30)
                });
        }));
});

var app = builder.Build();
app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"));

app.Run();

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 strony Razor , a nie do programów obsługi stron. Na przykład [EnableRateLimiting] nie można zastosować do OnGetprogramu , OnPostani ż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 metodę fixedPolicy stosowania ograniczania 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łonięcia [EnableRateLimiting("fixed")] stosowane do elementu i app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) wywoływane Home2Controller w Program.cspliku :

[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 [EnableRateLimiting("sliding")] element nie jest stosowany do Privacy metody akcji, ponieważ Program.cs o nazwie app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy).

Rozważ następujący kod, który nie wywołuje RequireRateLimitingMapRazorPages metody 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 "fixed" szybkości zasad jest stosowany do wszystkich metod akcji, które nie mają EnableRateLimiting atrybutów i DisableRateLimiting .
  • Limiter "sliding" szybkości zasad jest stosowany do Privacy akcji.
  • Ograniczanie szybkości jest wyłączone w metodzie NoLimit akcji.

Stosowanie atrybutów do Razor stron

W przypadku Razor stron atrybut musi być stosowany do strony Razor , a nie do programów obsługi stron. Na przykład [EnableRateLimiting] nie można zastosować do OnGetprogramu , OnPostani żadnego innego programu obsługi stron.

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 , OnRejectedRetryAfteriGlobalLimiter

Poniższy przykład:

  • Tworzy wywołanie zwrotne RateLimiterOptions.OnRejected , które jest wywoływane, gdy żądanie przekracza określony limit. retryAfter można używać z elementami TokenBucketRateLimiter, FixedWindowLimiteri SlidingWindowLimiter , ponieważ te algorytmy są w stanie oszacować, kiedy zostaną dodane więcej zezwoleń. Nie ConcurrencyLimiter ma możliwości obliczenia, kiedy zezwolenia będą dostępne.

  • Dodaje następujące ograniczniki:

    • Element SampleRateLimiterPolicy , który implementuje IRateLimiterPolicy<TPartitionKey> interfejs. Klasa jest wyświetlana SampleRateLimiterPolicy 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 IPAddresselementu .
// Preceding code removed for brevity.
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

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

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

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

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

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

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

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

        return new ValueTask();
    };

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

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

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

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

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

var app = builder.Build();

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

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

app.UseRouting();
app.UseRateLimiter();

app.UseAuthentication();
app.UseAuthorization();

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

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

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

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

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

app.Run();

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 użyto metody , OnRejected aby ustawić stan odpowiedzi na 429 Zbyt wiele żądań.OnRejectedContext Domyślny odrzucony stan to 503 Usługa niedostępna.

Ogranicznik z autoryzacją

W poniższym przykładzie użyto JStokenów internetowych ON (JWT) i utworzono partycję z tokenem 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 autoryzacją ConcurrencyLimiter, TokenBucketRateLimiteri

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