Megosztás a következőn keresztül:


Köztes szoftver sebességkorlátozása a ASP.NET Core-ban

Arvin Kahbazi, Maarten Balliauwés Rick Anderson

A Microsoft.AspNetCore.RateLimiting köztes szoftver sebességkorlátozó köztes szoftvereket biztosít. Az alkalmazások konfigurálják a sebességkorlátozó szabályzatokat, majd csatolják a szabályzatokat a végpontokhoz. A sebességkorlátozást használó alkalmazásokat az üzembe helyezés előtt gondosan be kell tölteni és felül kell vizsgálni. További információt a jelen cikkben A sebességkorlátozó végpontok tesztelése című cikkben talál.

Az aránykorlátozásról az közbenső szoftvercímű részben olvashat.

Sebességkorlátozó algoritmusok

A RateLimiterOptionsExtensions osztály a következő bővítménymetelyeket biztosítja a sebességkorlátozáshoz:

Rögzített ablakkorlátozó

A AddFixedWindowLimiter metódus rögzített időkeretet használ a kérelmek korlátozásához. Amikor az időkeret lejár, egy új időablak indul el, és a kérelemkorlát alaphelyzetbe áll.

Vegye figyelembe a következő kódot:

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

Az előző kód:

  • Meghívja AddRateLimiter, hogy adjon hozzá egy sebességkorlátozó szolgáltatást a szolgáltatásgyűjteményhez.
  • Meghívja a(z) AddFixedWindowLimiter-t, hogy hozzon létre egy rögzített ablakkorlátozót a(z) "fixed" házirend nevével, és beállítja a paramétereket:
  • PermitLimit 4-hez, az idő pedig Window 12-hez. Minden 12 másodperces ablakban legfeljebb 4 kérés engedélyezett.
  • QueueProcessingOrder és OldestFirst.
  • QueueLimit-tól 2-ig.
  • A UseRateLimiter függvény meghívása a sebességkorlátok engedélyezéséhez.

Az alkalmazásoknak konfigurációs kell használniuk a korlátozó beállítások megadásához. Az alábbi kód a konfigurációhoz MyRateLimitOptions használatával frissíti az előző kódot:

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MyRateLimitOptions>(
    builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter(policyName: fixedPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();

app.UseRateLimiter();

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

app.MapGet("/", () => Results.Ok($"Fixed Window Limiter {GetTicks()}"))
                           .RequireRateLimiting(fixedPolicy);

app.Run();

UseRateLimiter UseRouting után kell meghívni a végpontspecifikus API-k sebességkorlátozása esetén. Ha például a [EnableRateLimiting] attribútumot használja, UseRateLimiterUseRoutingután kell meghívni. Ha csak globális korlátokat hív meg, UseRateLimiter hívható meg UseRoutingelőtt.

Tolóablakkorlátozó

Egy tolóablak-algoritmus:

  • Hasonló a rögzített ablakkorlátozóhoz, de ablakonként szegmenseket ad hozzá. Az ablak minden szegmensintervallumban egy szegmenst csúsztat. A szegmens időköze (ablakidő)/(szegmensek ablakonként).
  • A kérések számának korlátozása egy ablak esetében permitLimit kérésre.
  • Minden időablak n szegmensekre van osztva ablakonként.
  • A lejárt időszegmensből visszavett kérelmek (n az aktuális szegmens előtti szegmensek) hozzáadódnak az aktuális szegmenshez. A leginkább lejárt időszakaszt, amely egy ablakkal korábbra esik, egyszerűen lejárt szegmensnek nevezzük.

Tekintse meg az alábbi táblázatot, amely egy 30 másodperces ablakkal rendelkező tolóablakkorlátozót, ablakonként három szegmenst és 100 kérelemkorlátot tartalmaz:

  • A felső sor és az első oszlop az időszegmenst jeleníti meg.
  • A második sor a fennmaradó elérhető kéréseket jeleníti meg. A fennmaradó kérelmeket a rendszer úgy számítja ki, hogy a rendelkezésre álló kérelmek mínusz a feldolgozott kérelmek és az újrafeldolgozott kérelmek.
  • A kérések mindig az átlós kék vonal mentén mozognak.
  • A 30. időponttól kezdve a lejárt időszakaszból származó kérés visszakerül a kérelemkorlátba, ahogy az a piros sorokban is látható.

kérelmeket, korlátokat és újrafeldolgozott pontokat bemutató táblázat

Az alábbi táblázat az előző gráf adatait mutatja be eltérő formátumban. Az Elérhető oszlop az előző szegmensből elérhető kéréseket jeleníti meg (az előző sorból származó átvitele). Az első sor 100 elérhető kérést jelenít meg, mert nincs korábbi szegmens.

Idő Beszerezhető Elfoglalt Lejárt anyagokból újrahasznosítva Átvitel
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

A következő kód a tolóablak sebességkorlátozóját használja:

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

Token vödrös korlátozó

A token vödör korlátozó hasonló a csúszóablak korlátozóhoz, de ahelyett, hogy a lejárt szegmensből származó kapcsolatokat visszaadná, minden feltöltési időszakban rögzített számú tokent ad hozzá. Az egyes szegmensekhez hozzáadott tokenek nem növelhetik a rendelkezésre álló tokenek számát a token vödör határa fölé. Az alábbi táblázat egy 100 jogkivonatot és egy 10 másodperces feltöltési időszakot tartalmazó jogkivonat gyűjtőkorlátozóját mutatja be.

Idő Beszerezhető Foglalt Hozzáadott Átvitel
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

A következő kód a token vödör korlátozót használja:

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

Ha AutoReplenishmenttrueértékre van állítva, a belső időzítő minden ReplenishmentPeriodfeltölti a jogkivonatokat; ha falseértékre van állítva, az alkalmazásnak meg kell hívnia a korláton TryReplenish.

Egyidejűségkorlátozó

Az egyidejűségkorlátozó korlátozza az egyidejű kérések számát. Minden kérelem egy-egy kérelemmel csökkenti az egyidejűségi korlátot. Amikor egy kérés befejeződik, a korlát eggyel nő. A többi kérelemkorlátozóval ellentétben, amelyek egy adott időszakra korlátozzák a kérelmek teljes számát, az egyidejűségkorlátozó csak az egyidejű kérelmek számát korlátozza, és nem korlátozza az adott időszakban a kérelmek számát.

Az alábbi kód az egyidejűségkorlátozót használja:

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

Láncolt korlátozók létrehozása

A CreateChained API több PartitionedRateLimiter átadását teszi lehetővé, amelyek egyetlen PartitionedRateLimiter-be kombinálva vannak. A kombinált korlátozó sorrendben futtatja az összes bemeneti korlátot.

A következő kód CreateChainedhasznál:

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

További információ: CreateChained forráskód

EnableRateLimiting és DisableRateLimiting attribútumok

A [EnableRateLimiting] és [DisableRateLimiting] attribútumok alkalmazhatók vezérlőre, műveletmetódusra vagy Razor oldalra. Razor Lapok esetében az attribútumot a Razor lapra kell alkalmazni, nem pedig az oldalkezelőkre. A [EnableRateLimiting] például nem alkalmazható OnGet, OnPostvagy más lapkezelőre.

A [DisableRateLimiting] attribútum letiltja sebességkorlátozást a vezérlőre, a műveleti módszerre vagy a Razor oldalra, függetlenül az elnevezett sebességkorlátozóktól vagy az alkalmazott globális korlátoktól. Vegyük például az alábbi kódot, amely meghívja RequireRateLimiting, hogy alkalmazza a fixedPolicy sebességkorlátozást az összes vezérlővégpontra:

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

Az alábbi kódban a [DisableRateLimiting] letiltja az áramláshatárolást, és felülbírálja a Program.cs-ben meghívott Home2Controller és app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) alkalmazott [EnableRateLimiting("fixed")]-et.

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

Az előző kódban a [EnableRateLimiting("sliding")]nincs alkalmazva a Privacy műveleti metódusra, mert Program.csapp.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy).

Fontolja meg a következő kódot, amely nem hívja meg a RequireRateLimiting-át a MapRazorPages-en vagy a MapDefaultControllerRoute-n:

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

Vegye figyelembe a következő vezérlőt:

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

Az előző vezérlőben:

  • A "fixed" szabályzat sebességkorlátozóját minden olyan műveletmetelyre alkalmazza a rendszer, amely nem rendelkezik EnableRateLimiting és DisableRateLimiting attribútumokkal.
  • A "sliding" szabályzat sebességkorlátozóját alkalmazza a rendszer a Privacy műveletre.
  • A sebességkorlátozás le van tiltva a NoLimit műveletmetódusban.

Attribútumok alkalmazása Razor Lapokra

Razor Lapok esetében az attribútumot a Razor lapra kell alkalmazni, nem pedig az oldalkezelőkre. A [EnableRateLimiting] például nem alkalmazható OnGet, OnPostvagy más lapkezelőre.

A DisableRateLimiting attribútum letiltja a sebességkorlátozást egy Razor oldalon. EnableRateLimiting csak akkor alkalmazza a rendszer egy Razor lapra, ha MapRazorPages().RequireRateLimiting(Policy) nem lett meghívva.

Limiter algoritmusok összehasonlítása

A rögzített, a csúsztatási és a token-korlátozók mindegyike korlátozza a kérelmek maximális számát egy adott időszakban. Az egyidejűségkorlátozó csak az egyidejű kérések számát korlátozza, és nem korlátozza a kérelmek számát egy adott időszakban. A végpont költségét figyelembe kell venni a korlátozó kiválasztásakor. A végpont költsége magában foglalja a felhasznált erőforrásokat, például az időt, az adathozzáférést, a CPU-t és az I/O-t.

Sebességkorlátozó minták

Az alábbi minták nem éles kódhoz vannak szánva, hanem példák a korlátozók használatára.

Limiter OnRejected, RetryAfterés GlobalLimiter

A következő minta:

  • Létrehoz egy RateLimiterOptions.OnRejected visszahívást, amely akkor lesz meghívva, ha egy kérés túllépi a megadott korlátot. A retryAfter használható a TokenBucketRateLimiter-vel, a FixedWindowLimiter-rel és a SlidingWindowLimiter-rel, mert ezek az algoritmusok képesek megbecsülni, hogy mikor adódnak hozzá új engedélyek. A ConcurrencyLimiter-nak nincs módja kiszámítani, hogy mikor lesznek elérhetők az engedélyek.

  • A következő korlátokat adja hozzá:

    • A IRateLimiterPolicy<TPartitionKey> felületet megvalósító SampleRateLimiterPolicy. A SampleRateLimiterPolicy osztály a cikk későbbi részében jelenik meg.
    • Egy SlidingWindowLimiter:
      • Minden hitelesített felhasználóhoz egy partícióval.
      • Egy megosztott partíció az összes névtelen felhasználó számára.
    • Az GlobalLimiter, amely az összes kérelemre alkalmazva van. A rendszer először a globális korlátozót hajtja végre, majd a végpontspecifikus korlátozót, ha létezik ilyen. A GlobalLimiter létrehoz egy partíciót minden IPAddressszámára.
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();

Figyelmeztetés

Az ügyfél IP-címeinek partícióinak létrehozása sebezhetővé teszi az alkalmazást a szolgáltatásmegtagadási támadásokkal szemben, amelyek IP-forráscím-hamisítást alkalmaznak. További információért lásd: BCP 38 RFC 2827 Hálózati bejövő forgalom szűrés: A szolgáltatásmegtagadási támadások legyőzése, amelyek IP-cím hamisítást alkalmaznak.

A teljes fájlhoz tekintse meg mintaadattárat.

A SampleRateLimiterPolicy osztály

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

Az előző kódban a OnRejected a OnRejectedContext használatával állítja be a válasz státuszát a 429 Túl Sok Kérés. Az alapértelmezett elutasított állapot 503 Szolgáltatás nem érhető el.

Limiter engedélyezéssel

Az alábbi minta JSON webes jogkivonatokat (JWT) használ, és létrehoz egy partíciót a JWT hozzáférési jogkivonattal. Éles alkalmazásokban a JWT-t általában egy biztonsági jogkivonat-szolgáltatásként (STS) működő kiszolgáló biztosítja. A helyi fejlesztéshez a dotnet user-jwts parancssori eszköz használható alkalmazásspecifikus helyi JWT-k létrehozásához és kezeléséhez.

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 az ConcurrencyLimiter, TokenBucketRateLimiterés engedélyezés használatával

A következő minta:

  • Hozzáad egy ConcurrencyLimiter-t a Razor oldalakon használt "get" házirendnevével.
  • Hozzáad egy TokenBucketRateLimiter-t, amelyhez egy partíció tartozik minden jogosult felhasználónak és egy közös partíció az összes névtelen felhasználónak.
  • A RateLimiterOptions.RejectionStatusCode beállítása a 429-túl sok kérés.
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
            });
    }));

A teljes fájlhoz tekintse meg mintaadattárat.

Végpontok tesztelése sebességkorlátozással

Mielőtt üzembe helyezi egy alkalmazást sebességkorlátozás használatával az éles környezetben, végezzen terhelési tesztelést az alkalmazáson a sebességkorlátozók és a használt lehetőségek érvényesítéséhez. Hozzon létre például egy JMeter-szkriptet egy olyan eszközzel, mint BlazeMeter vagy Apache JMeter HTTP(S) tesztszkriptrögzítő, és töltse be a szkriptet az Azure Load Testing.

A felhasználói bemenettel rendelkező partíciók létrehozása sebezhetővé teszi az alkalmazást Szolgáltatásmegtagadási (DoS) támadásokkal szemben. Például az ügyfél IP-címeinek partícióinak létrehozása sebezhetővé teszi az alkalmazást az IP-forráscím-hamisítást alkalmazó szolgáltatásmegtagadási támadásokkal szemben. További információért lásd: BCP 38 RFC 2827 Hálózati bejövő forgalom szűrése: IP forráscím hamisításával végrehajtott szolgáltatásmegtagadási támadások kivédése.

További erőforrások