Delen via


Snelheidsbeperking middleware in ASP.NET Core

Door Arvin Kahbazi, Maarten Balliauwen Rick Anderson

De Microsoft.AspNetCore.RateLimiting middleware biedt snelheidsbeperkende middleware. Apps configureren beleidsregels voor snelheidsbeperking en voegen vervolgens het beleid toe aan eindpunten. Apps die frequentielimiet gebruiken, moeten zorgvuldig worden getest en gecontroleerd voordat ze worden geïmplementeerd. Zie Testeindpunten met snelheidsbeperking in dit artikel voor meer informatie.

Zie Rate limiting middlewarevoor een inleiding tot snelheidsbeperking.

Algoritmen voor frequentielimieten

De RateLimiterOptionsExtensions-klasse biedt de volgende uitbreidingsmethoden voor snelheidsbeperking:

Vaste vensterbegrenzer

De methode AddFixedWindowLimiter gebruikt een vast tijdvenster om aanvragen te beperken. Wanneer het tijdvenster verloopt, wordt een nieuw tijdvenster gestart en wordt de aanvraaglimiet opnieuw ingesteld.

Houd rekening met de volgende code:

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

De voorgaande code:

  • Roept AddRateLimiter aan om een snelheidsbeperkingsservice toe te voegen aan de serviceverzameling.
  • Roept AddFixedWindowLimiter aan om een vaste vensterbegrenzer te maken met een beleidsnaam van "fixed" en sets:
  • PermitLimit tot 4 en de tijd Window tot 12. Er zijn maximaal 4 aanvragen per elk venster van 12 seconden toegestaan.
  • QueueProcessingOrder tot OldestFirst.
  • QueueLimit tot 2.
  • Roept UseRateLimiter- aan om snelheidsbeperking in te schakelen.

Apps moeten Configuration gebruiken om opties voor limieten in te stellen. Met de volgende code wordt de voorgaande code bijgewerkt met behulp van MyRateLimitOptions voor configuratie:

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 moet worden aangeroepen na UseRouting wanneer eindpuntspecifieke API's voor frequentiebeperking worden gebruikt. Als het kenmerk [EnableRateLimiting] bijvoorbeeld wordt gebruikt, moet UseRateLimiter worden aangeroepen na UseRouting. Wanneer u alleen globale begrenzers aanroept, kan UseRateLimiter worden aangeroepen voordat UseRouting.

Schuifvensterbegrenzer

Een algoritme voor een schuifvenster:

  • Is vergelijkbaar met de vaste vensterbegrenzer, maar voegt segmenten per venster toe. Het venster schuift één segment per segmentinterval. Het segmentinterval is (tijdvenster)/(segmenten per venster).
  • Hiermee beperkt u het aantal aanvragen voor een venster tot permitLimit.
  • Elke tijdvenster wordt verdeeld in n segmenten per venster.
  • Aanvragen uit het verlopen tijdsegment één venster terug (n segment vóór het huidige segment) worden toegevoegd aan het huidige segment. We verwijzen naar het meest verlopen tijdsegment één venster terug als het verlopen segment.

Bekijk de volgende tabel met een schuifvensterbegrenzer met een venster van 30 seconden, drie segmenten per venster en een limiet van 100 aanvragen:

  • In de bovenste rij en eerste kolom wordt het tijdsegment weergegeven.
  • In de tweede rij ziet u de resterende aanvragen die beschikbaar zijn. De resterende aanvragen worden berekend als de beschikbare aanvragen minus de verwerkte aanvragen plus de gerecyclede aanvragen.
  • Aanvragen bewegen elke keer langs de diagonale blauwe lijn.
  • Vanaf tijd 30 wordt de aanvraag uit het verlopen tijdsegment weer toegevoegd aan de aanvraaglimiet, zoals wordt weergegeven in de rode regels.

Tabel met verzoeken, limieten en gerecyclede sleuven

In de volgende tabel ziet u de gegevens in de vorige grafiek in een andere indeling. In de kolom Beschikbaar worden de aanvragen weergegeven die beschikbaar zijn in het vorige segment (de uit de vorige rij overdragen). In de eerste rij ziet u 100 beschikbare aanvragen omdat er geen vorig segment is.

Tijd Beschikbaar Gepakt Gerecycled van verlopen materialen Overdracht
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

De volgende code maakt gebruik van de frequentielimiet voor schuifvensters:

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 bucket-begrenzer

De tokenbucketbegrenzer is vergelijkbaar met de schuifvensterbegrenzer, maar in plaats van de aanvragen uit het verlopen segment toe te voegen, wordt elke aanvullingsperiode een vast aantal tokens toegevoegd. De tokens die aan elk segment zijn toegevoegd, kunnen de beschikbare tokens niet verhogen tot een getal dat hoger is dan de limiet voor de tokenbucket. In de volgende tabel ziet u een tokenbucketbegrenzer met een limiet van 100 tokens en een aanvullingsperiode van 10 seconden.

Tijd Beschikbaar Genomen Toegevoegd Overdracht
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

De volgende code maakt gebruik van de bucketbegrenzer voor tokens:

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

Wanneer AutoReplenishment is ingesteld op true, vult een interne timer de tokens aan met een interval van ReplenishmentPeriod; als deze is ingesteld op false, moet de app TryReplenish aanroepen op de limiter.

Limiet voor gelijktijdigheid

De gelijktijdigheidsbeperking beperkt het aantal gelijktijdige aanvragen. Elke aanvraag vermindert de gelijktijdigheidslimiet met één. Wanneer een aanvraag is voltooid, wordt de limiet met één verhoogd. In tegenstelling tot de andere limieten voor aanvragen die het totale aantal aanvragen voor een opgegeven periode beperken, beperkt de limiet voor gelijktijdigheid alleen het aantal gelijktijdige aanvragen en wordt het aantal aanvragen in een bepaalde periode niet beperkt.

De volgende code maakt gebruik van de gelijktijdigheidslimiet:

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

Gekoppelde begrenzers maken

Met de CreateChained-API kunnen meerdere PartitionedRateLimiter worden doorgegeven die in één PartitionedRateLimiterworden gecombineerd. Met de gecombineerde begrenzer worden alle invoerbegrenzers op volgorde uitgevoerd.

De volgende code maakt gebruik van CreateChained:

using System.Globalization;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

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

        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", cancellationToken);
    };
    _.GlobalLimiter = PartitionedRateLimiter.CreateChained(
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();

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

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

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

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

app.Run();

Zie de CreateChained-broncode voor meer informatie

kenmerken EnableRateLimiting en DisableRateLimiting

De kenmerken [EnableRateLimiting] en [DisableRateLimiting] kunnen worden toegepast op een controller, actiemethode of Razor-pagina. Voor Razor Pagina's moet het kenmerk worden toegepast op de Razor Pagina en niet op de pagina-handlers. [EnableRateLimiting] kan bijvoorbeeld niet worden toegepast op OnGet, OnPostof een andere pagina-handler.

Het kenmerk [DisableRateLimiting]schakelt snelheidsbeperking voor controller, actiemethode of Razor pagina uit, ongeacht de benoemde frequentielimieten of globale limieten die zijn toegepast. Denk bijvoorbeeld aan de volgende code die RequireRateLimiting aanroept om de fixedPolicy frequentielimiet toe te passen op alle controllereindpunten:

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

In de volgende code schakelt [DisableRateLimiting] snelheidsbeperking uit en negeert de [EnableRateLimiting("fixed")] die wordt toegepast op de Home2Controller en app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) die in Program.csworden aangeroepen.

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

In de voorgaande code wordt de [EnableRateLimiting("sliding")] niet toegepast op de actiemethode Privacy omdat Program.csapp.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)genoemd.

Houd rekening met de volgende code die geen RequireRateLimiting aanroept op MapRazorPages of 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();

Houd rekening met de volgende controller:

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

In de voorgaande controller:

  • De "fixed" beleidsfrequentielimiet wordt toegepast op alle actiemethoden die geen EnableRateLimiting en DisableRateLimiting kenmerken hebben.
  • De "sliding" beleidssnelheidslimiet wordt toegepast op de actie Privacy.
  • Snelheidsbeperking is uitgeschakeld voor de actiemethode NoLimit.

Kenmerken toepassen op Razor pagina’s

Voor Razor Pagina's moet het kenmerk worden toegepast op de Razor Pagina en niet op de pagina-handlers. [EnableRateLimiting] kan bijvoorbeeld niet worden toegepast op OnGet, OnPostof een andere pagina-handler.

Het kenmerk DisableRateLimiting schakelt snelheidsbeperking op een Razor-pagina uit. wordt alleen op een Pagina toegepast als niet is aangeroepen.

Vergelijking van beperkende algoritmen

De vaste, schuif- en tokenbegrenzers beperken allemaal het maximum aantal aanvragen in een bepaalde periode. De gelijktijdigheidsbeperking beperkt alleen het aantal gelijktijdige aanvragen en beperkt het aantal aanvragen in een bepaalde periode niet. De kosten van een eindpunt moeten worden overwogen bij het selecteren van een begrenzer. De kosten van een eindpunt omvatten de gebruikte resources, bijvoorbeeld tijd, gegevenstoegang, CPU en I/O.

Voorbeelden van frequentielimieten

De volgende voorbeelden zijn niet bedoeld voor productiecode, maar zijn voorbeelden van het gebruik van de limieten.

Begrenzer met OnRejected, RetryAfteren GlobalLimiter

Het volgende voorbeeld:

  • Hiermee maakt u een RateLimiterOptions.OnRejected callback die wordt aangeroepen wanneer een aanvraag de opgegeven limiet overschrijdt. retryAfter kan worden gebruikt met de TokenBucketRateLimiter, FixedWindowLimiteren SlidingWindowLimiter omdat deze algoritmen kunnen schatten wanneer er meer vergunningen worden toegevoegd. De ConcurrencyLimiter heeft geen manier om te berekenen wanneer vergunningen beschikbaar zijn.

  • Voegt de volgende limieten toe:

    • Een SampleRateLimiterPolicy waarmee de IRateLimiterPolicy<TPartitionKey>-interface wordt geïmplementeerd. De SampleRateLimiterPolicy klasse wordt verderop in dit artikel weergegeven.
    • Een SlidingWindowLimiter:
      • Met een partitie voor elke geverifieerde gebruiker.
      • Eén gedeelde partitie voor alle anonieme gebruikers.
    • Een GlobalLimiter die wordt toegepast op alle aanvragen. De globale begrenzer wordt eerst uitgevoerd, gevolgd door de eindpuntspecifieke begrenzer, als er een bestaat. De GlobalLimiter maakt een partitie voor elke IPAddress.
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

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

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

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

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

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

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

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

        return new ValueTask();
    };

    limiterOptions.AddPolicy<string, SampleRateLimiterPolicy>(helloPolicy);
    limiterOptions.AddPolicy(userPolicyName, context =>
    {
        var username = "anonymous user";
        if (context.User.Identity?.IsAuthenticated is true)
        {
            username = context.User.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();

Waarschuwing

Als u partities maakt op client-IP-adressen, is de app kwetsbaar voor Denial of Service-aanvallen die gebruikmaken van IP-bronadresvervalsing. Zie BCP 38 RFC 2827 Network Ingress Filtering: Denial of Service-aanvallen verslaan die gebruikmaken van IP-bronadresvervalsingvoor meer informatie.

Zie de opslagplaats met voorbeelden voor het volledige Program.cs-bestand.

De SampleRateLimiterPolicy-klasse

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

In de voorgaande code gebruikt OnRejectedOnRejectedContext om de antwoordstatus in te stellen op 429 Te veel aanvragen. De standaard geweigerde status is 503 Service Unavailable.

Begrenzer met autorisatie

In het volgende voorbeeld wordt JSON-webtokens (JWT) gebruikt en wordt een partitie gemaakt met het JWT-toegangstoken. In een productie-app wordt de JWT doorgaans geleverd door een server die fungeert als een beveiligingstokenservice (STS). Voor lokale ontwikkeling kan het dotnet user-jwts opdrachtregelprogramma worden gebruikt om app-specifieke lokale JWT's te maken en te beheren.

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

Begrenzer met ConcurrencyLimiter, TokenBucketRateLimiteren autorisatie

Het volgende voorbeeld:

  • Hiermee voegt u een ConcurrencyLimiter toe met een beleidsnaam van "get" die wordt gebruikt op de Razor Pagina's.
  • Voegt een TokenBucketRateLimiter toe met een partitie voor elke geautoriseerde gebruiker en een partitie voor alle anonieme gebruikers.
  • Stelt RateLimiterOptions.RejectionStatusCode in op 429 Te veel verzoeken.
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
            });
    }));

Zie de opslagplaats met voorbeelden voor het volledige Program.cs-bestand.

Eindpunten testen met snelheidsbeperking

Voordat u een app implementeert met snelheidsbeperking voor productie, test u de app met stress om de frequentielimieten en gebruikte opties te valideren. Maak bijvoorbeeld een JMeter-script met een hulpprogramma zoals BlazeMeter of Apache JMeter HTTP(S) Test Script Recorder en laad het script naar Azure Load Testing.

Als u partities maakt met gebruikersinvoer, is de app kwetsbaar voor DoS-aanvallen (Denial of Service). Als u bijvoorbeeld partities maakt op client-IP-adressen, is de app kwetsbaar voor Denial of Service-aanvallen die gebruikmaken van IP-bronadresvervalsing. Zie voor meer informatie BCP 38 RFC 2827 Netwerktoegangsfiltering: Het tegengaan van Denial of Service-aanvallen die IP-adresvervalsing gebruiken.

Aanvullende informatiebronnen