Sdílet prostřednictvím


Omezování rychlosti middlewaru v ASP.NET Core

Arvin Kahbazi, Maarten Balliauw a Rick Anderson

Middleware Microsoft.AspNetCore.RateLimiting poskytuje omezování rychlosti middlewaru. Aplikace konfigurují zásady omezování rychlosti a pak zásady připojují ke koncovým bodům. Aplikace využívající omezování rychlosti by se měly před nasazením pečlivě testovat a kontrolovat. Další informace najdete v části Testování koncových bodů s omezováním rychlosti v tomto článku.

Úvod do omezování rychlosti najdete v tématu Omezování rychlosti middleware.

Algoritmy omezování rychlosti

Třída RateLimiterOptionsExtensions poskytuje následující rozšiřující metody pro omezování rychlosti:

Oprava omezovače oken

Metoda AddFixedWindowLimiter používá k omezení požadavků pevný časový interval. Po vypršení časového intervalu se spustí nové časové okno a limit požadavku se resetuje.

Uvažujte následující kód:

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

Předchozí kód:

Aplikace by měly používat konfiguraci k nastavení možností omezovače. Následující kód aktualizuje předchozí kód pomocí MyRateLimitOptions konfigurace:

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 musí být volána po UseRouting použití rozhraní API pro omezení rychlosti pro konkrétní koncové body. Pokud je například [EnableRateLimiting] použit atribut, UseRateLimiter musí být volána za UseRouting. Při volání pouze globálních limiterů UseRateLimiter lze volat před UseRouting.

Posuvný omezovač oken

Algoritmus posuvného okna:

  • Podobá se omezovači pevných oken, ale přidává segmenty na okno. Okno se posune o jeden segment každého intervalu segmentů. Interval segmentu je (čas okna)/(segmenty na okno).
  • Omezuje požadavky na okno na permitLimit žádosti.
  • Každé časové okno je rozděleno do n segmentů na každé okno.
  • Do aktuálního segmentu se přidají žádosti odebrané z časového segmentu s vypršenou platností (n segmenty před aktuálním segmentem). Jako segment s vypršenou platností se odkazujeme na segment, jehož platnost vypršela.

Podívejte se na následující tabulku, která ukazuje omezovač posuvného okna s 30sekundovým oknem, třemi segmenty na okno a limitem 100 požadavků:

  • Horní řádek a první sloupec znázorňují časový segment.
  • Druhý řádek zobrazuje zbývající dostupné žádosti. Zbývající požadavky se počítají jako dostupné požadavky minus zpracované požadavky a recyklované žádosti.
  • Žádosti se vždy pohybují podél diagonální modré čáry.
  • Od 30. okamžiku se žádost převzatá z časového segmentu s vypršenou platností přidá zpět do limitu požadavku, jak je znázorněno na červených řádcích.

Table showing requests, limits, and recycled slots

Následující tabulka ukazuje data v předchozím grafu v jiném formátu. Sloupec K dispozici zobrazuje požadavky dostupné z předchozího segmentu (Přenos z předchozího řádku). První řádek zobrazuje 100 dostupných požadavků, protože neexistuje žádný předchozí segment.

Čas dostupný Přijata Recyklace z konce platnosti Přenos
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

Následující kód používá omezovač rychlosti posuvného okna:

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

Omezovač kontejnerů tokenů

Limitátor kontejneru tokenů je podobný klouzavému omezovači oken, ale místo přidání žádostí přijatých z segmentu s vypršenou platností se každý interval doplňování přidá pevný počet tokenů. Přidané tokeny nemohou zvýšit dostupné tokeny na číslo vyšší než limit kontejneru tokenů. Následující tabulka ukazuje limit kontejneru tokenů s limitem 100 tokenů a 10sekundovým doplňovacím obdobím.

Čas dostupný Přijata Přidáno Přenos
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

Následující kód používá omezovač kontejneru tokenů:

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

Pokud AutoReplenishment je nastavena hodnota true, vnitřní časovač doplní tokeny vždy ReplenishmentPeriod; pokud je nastavena falsena , aplikace musí volat TryReplenish limiter.

Omezovač souběžnosti

Limiter souběžnosti omezuje počet souběžných požadavků. Každý požadavek snižuje limit souběžnosti o jeden. Po dokončení žádosti se limit zvýší o jeden. Na rozdíl od ostatních omezovačů požadavků, které omezují celkový počet požadavků na zadané období, limiter souběžnosti omezuje pouze počet souběžných požadavků a nesdílí počet požadavků v časovém období.

Následující kód používá limiter souběžnosti:

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

Vytváření zřetězených omezovačů

Rozhraní CreateChained API umožňuje předávání více PartitionedRateLimiter , které jsou sloučeny do jednoho PartitionedRateLimiter. Kombinovaný limiter spouští všechny vstupní limitery v posloupnosti.

Následující kód používá 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();

Další informace najdete ve zdrojovém kódu CreateChained.

EnableRateLimiting a DisableRateLimiting atributy

Atributy [EnableRateLimiting] lze [DisableRateLimiting] použít u kontroleru, metody akce nebo Razor stránky. U Razor stránek musí být atribut použit na Razor stránku, nikoli na obslužné rutiny stránky. [EnableRateLimiting] Například nelze použít pro OnGet, OnPostani pro žádnou jinou obslužnou rutinu stránky.

Atribut [DisableRateLimiting]zakáže omezení rychlosti kontroleru, metody akce nebo Razor stránky bez ohledu na pojmenované omezovače rychlosti nebo globální limitery použité. Představte si například následující kód, který volá RequireRateLimiting , aby se omezení rychlosti použilo fixedPolicy na všechny koncové body kontroleru:

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

V následujícím kódu [DisableRateLimiting] zakáže omezování rychlosti a přepsání [EnableRateLimiting("fixed")] použité u Home2Controllerapp.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) a volána :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 });
    }
}

V předchozím kódu [EnableRateLimiting("sliding")] se nepoužije na metodu Privacy akce, protože Program.cs volána app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy).

Představte si následující kód, který nezavolá RequireRateLimitingMapRazorPages nebo 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();

Vezměte v úvahu následující 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 });
    }
}

V předchozím kontroleru:

  • Omezení "fixed" rychlosti zásad se použije u všech metod akcí, které nemají EnableRateLimiting a DisableRateLimiting nemají atributy.
  • U "sliding" akce se použije Privacy omezení rychlosti zásad.
  • Omezení rychlosti je u metody akce zakázané NoLimit .

Použití atributů na Razor stránky

U Razor stránek musí být atribut použit na Razor stránku, nikoli na obslužné rutiny stránky. [EnableRateLimiting] Například nelze použít pro OnGet, OnPostani pro žádnou jinou obslužnou rutinu stránky.

Atribut DisableRateLimiting zakáže omezování rychlosti na Razor stránce. EnableRateLimiting je použita pouze na Razor stránku, pokud MapRazorPages().RequireRateLimiting(Policy)nebyla volána.

Porovnání limiterů algoritmů

Pevné, posuvné a tokenové omezovače omezují maximální počet požadavků v časovém období. Limiter souběžnosti omezuje pouze počet souběžných požadavků a neskončuje počet požadavků v časovém období. Náklady na koncový bod by se měly zvážit při výběru limiteru. Náklady na koncový bod zahrnují použité prostředky, například čas, přístup k datům, procesor a vstupně-výstupní operace.

Vzorky omezovače rychlosti

Následující ukázky nejsou určené pro produkční kód, ale představují příklady použití omezovačů.

Limiter s OnRejected, RetryAftera GlobalLimiter

Následující ukázka:

  • Vytvoří zpětné volání RateLimiterOptions.OnRejected, které se volá, když požadavek překročí zadaný limit. retryAfter lze použít s TokenBucketRateLimiter, FixedWindowLimitera SlidingWindowLimiter protože tyto algoritmy jsou schopny odhadnout, kdy bude přidáno více povolení. Nemá ConcurrencyLimiter žádný způsob výpočtu, kdy bude povolení k dispozici.

  • Přidá následující limitery:

    • A SampleRateLimiterPolicy , která implementuje IRateLimiterPolicy<TPartitionKey> rozhraní. Třída SampleRateLimiterPolicy se zobrazí dále v tomto článku.
    • A SlidingWindowLimiter:
      • S oddílem pro každého ověřeného uživatele.
      • Jeden sdílený oddíl pro všechny anonymní uživatele.
    • A GlobalLimiter , který se použije na všechny požadavky. Globální limiter se spustí jako první, za ním bude následovat limiter specifický pro koncový bod, pokud existuje. Vytvoří GlobalLimiter oddíl pro každý IPAddress.
// Preceding code removed for brevity.
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

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

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

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

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

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

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

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

        return new ValueTask();
    };

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

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

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

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

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

var app = builder.Build();

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

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

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

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

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

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

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

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

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

app.Run();

Upozorňující

Při vytváření oddílů na IP adresách klienta je aplikace zranitelná vůči útokům na dostupnost služby, které využívají falšování identity zdrojové IP adresy. Další informace najdete v tématu BCP 38 RFC 2827 Filtrování příchozího přenosu dat sítě: Porazit útoky na odepření služby, které využívají falšování identity zdrojové IP adresy.

Kompletní soubor najdete v Program.cs úložišti ukázek.

Třída 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
            });
    }
}

V předchozím kódu OnRejected slouží OnRejectedContext k nastavení stavu odpovědi na hodnotu 429 Příliš mnoho požadavků. Výchozí odmítnutý stav je 503 Služba není k dispozici.

Limiter s autorizací

Následující ukázka používá JSON Web Tokens (JWT) a vytvoří oddíl s přístupovým tokenem JWT. V produkční aplikaci by JWT obvykle poskytoval server fungující jako služba tokenů zabezpečení (STS). Pro místní vývoj lze nástroj příkazového řádku dotnet user-jwts použít k vytvoření a správě místních JWT specifických pro aplikaci.

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

Limiter s ConcurrencyLimiter, TokenBucketRateLimitera autorizace

Následující ukázka:

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

Kompletní soubor najdete v Program.cs úložišti ukázek.

Testování koncovýchbodůch

Před nasazením aplikace pomocí omezování rychlosti do produkčního prostředí otestujte aplikaci a ověřte použité omezení rychlosti a možnosti. Vytvořte například skript JMeter pomocí nástroje, jako je BlazeMeter nebo Apache JMeter HTTP(S) Test Script Recorder a načtěte skript do služby Azure Load Testing.

Vytváření oddílů se vstupem uživatele způsobí, že aplikace bude zranitelná vůči útokům DoS (Denial of Service ). Vytváření oddílů na IP adresách klienta například způsobí, že aplikace bude zranitelná vůči útokům na dostupnost služby, které využívají falšování identity zdrojové IP adresy. Další informace najdete v tématu BCP 38 RFC 2827 Filtrování příchozího přenosu dat sítě: Porazit útoky na odepření služeb, které využívají falšování identity zdrojové IP adresy.

Další materiály