Condividi tramite


Middleware di limite di frequenza in ASP.NET Core

Di Arvin Kahbazi, Maarten Balliauw e Rick Anderson

Il middleware fornisce middleware di limitazione della Microsoft.AspNetCore.RateLimiting velocità. Le app configurano i criteri di limitazione della frequenza e quindi allegano i criteri agli endpoint. Le app che usano la limitazione della frequenza devono essere testate e esaminate attentamente prima della distribuzione. Per altre informazioni, vedere Test degli endpoint con limitazione della frequenza in questo articolo.

Per un'introduzione alla limitazione della frequenza, vedere Middleware di limitazione della frequenza.

Algoritmi limiter di frequenza

La RateLimiterOptionsExtensions classe fornisce i metodi di estensione seguenti per la limitazione della frequenza:

Limite di finestra fisso

Il AddFixedWindowLimiter metodo usa un intervallo di tempo fisso per limitare le richieste. Quando scade l'intervallo di tempo, viene avviato un nuovo intervallo di tempo e viene reimpostato il limite di richieste.

Osservare il codice seguente:

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

Il codice precedente:

  • Chiama AddRateLimiter per aggiungere un servizio di limitazione della frequenza alla raccolta di servizi.
  • Chiamate AddFixedWindowLimiter per creare un limiter di finestra fisso con un nome di criteri e "fixed" set:
  • PermitLimit a 4 e il tempo Window a 12. È consentito un massimo di 4 richieste per ogni finestra di 12 secondi.
  • Da QueueProcessingOrder a OldestFirst.
  • QueueLimit a 2.
  • Chiama UseRateLimiter per abilitare la limitazione della frequenza.

Le app devono usare Configuration per impostare le opzioni limiter. Il codice seguente aggiorna il codice precedente usando MyRateLimitOptions per la configurazione:

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 deve essere chiamato dopo UseRouting l'uso delle API specifiche dell'endpoint di limitazione della frequenza. Ad esempio, se viene usato l'attributo [EnableRateLimiting] , UseRateLimiter deve essere chiamato dopo UseRouting. Quando si chiamano solo i limiter globali, UseRateLimiter è possibile chiamare prima UseRoutingdi .

Limite finestra scorrevole

Algoritmo finestra scorrevole:

  • È simile al limite di finestra fisso, ma aggiunge segmenti per finestra. La finestra scorre un segmento a ogni intervallo di segmento. L'intervallo di segmento è (intervallo di tempo)/(segmenti per finestra).
  • Limita le richieste di una finestra alle permitLimit richieste.
  • Ogni intervallo di tempo è diviso in n segmenti per finestra.
  • Le richieste prelevate dal segmento di tempo scaduto indietro (n segmenti precedenti al segmento corrente) vengono aggiunte al segmento corrente. Si fa riferimento al segmento di tempo più scaduto di nuovo come segmento scaduto.

Si consideri la tabella seguente che mostra un limite di finestra scorrevole con una finestra di 30 secondi, tre segmenti per finestra e un limite di 100 richieste:

  • La riga superiore e la prima colonna mostrano il segmento di tempo.
  • La seconda riga mostra le richieste rimanenti disponibili. Le richieste rimanenti vengono calcolate come richieste disponibili meno le richieste elaborate più le richieste riciclate.
  • Le richieste in ogni momento si spostano lungo la linea blu diagonale.
  • Dall'ora 30 in poi, la richiesta ottenuta dal segmento di tempo scaduto viene aggiunta nuovamente al limite della richiesta, come illustrato nelle righe rosse.

Table showing requests, limits, and recycled slots

La tabella seguente mostra i dati nel grafico precedente in un formato diverso. La colonna Available (Disponibile) mostra le richieste disponibili dal segmento precedente (Il trasporto dalla riga precedente). La prima riga mostra 100 richieste disponibili perché non è presente alcun segmento precedente.

Ora Disponibile Preso Riciclato da scaduto Trasportare
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

Il codice seguente usa il limite di frequenza delle finestre scorrevoli:

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

Limite del bucket del token

Il limite del bucket del token è simile al limite della finestra scorrevole, ma invece di aggiungere di nuovo le richieste effettuate dal segmento scaduto, viene aggiunto un numero fisso di token ogni periodo di rifornimento. I token aggiunti a ogni segmento non possono aumentare i token disponibili a un numero superiore al limite del bucket del token. La tabella seguente illustra un limite di bucket di token con un limite di 100 token e un periodo di rifornimento di 10 secondi.

Ora Disponibile Preso Aggiunto Trasportare
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

Il codice seguente usa il limite del bucket del 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();

Quando AutoReplenishment è impostato su true, un timer interno ricostituisce i token ogni ReplenishmentPeriod; se impostato su false, l'app deve chiamare TryReplenish sul limite.

Limite di concorrenza

Il limite di concorrenza limita il numero di richieste simultanee. Ogni richiesta riduce il limite di concorrenza di uno. Al termine di una richiesta, il limite viene aumentato di uno. A differenza degli altri limiter di richieste che limitano il numero totale di richieste per un periodo specificato, il limite di concorrenza limita solo il numero di richieste simultanee e non limita il numero di richieste in un periodo di tempo.

Il codice seguente usa il limite di concorrenza:

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

Creare limiti concatenati

L'API CreateChained consente il passaggio di più PartitionedRateLimiter elementi combinati in un unico PartitionedRateLimiteroggetto . Il limiter combinato esegue tutti i limiter di input in sequenza.

Il codice seguente usa 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();

Per altre informazioni, vedere il codice sorgente CreateChained

Attributi EnableRateLimiting e DisableRateLimiting

Gli [EnableRateLimiting] attributi e [DisableRateLimiting] possono essere applicati a un controller, a un metodo di azione o Razor a una pagina. Per Razor Pages, l'attributo deve essere applicato alla Razor pagina e non ai gestori di pagina. Ad esempio, [EnableRateLimiting] non può essere applicato a OnGet, OnPosto a qualsiasi altro gestore di pagine.

L'attributo [DisableRateLimiting]disabilita la limitazione della frequenza al controller, al metodo di azione o Razor alla pagina indipendentemente dai limiti di frequenza denominati o dai limiter globali applicati. Si consideri ad esempio il codice seguente che chiama RequireRateLimiting per applicare la limitazione della fixedPolicy frequenza a tutti gli endpoint controller:

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

Nel codice [DisableRateLimiting] seguente disabilita la limitazione della frequenza e gli override [EnableRateLimiting("fixed")] applicati a Home2Controller e app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) chiamati in 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 });
    }
}

Nel codice precedente, l'oggetto [EnableRateLimiting("sliding")] non viene applicato al Privacy metodo di azione perché Program.cs denominato app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy).

Si consideri il codice seguente che non chiama RequireRateLimiting o MapRazorPagesMapDefaultControllerRoute:

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

Prendere in considerazione il controller seguente:

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

Nel controller precedente:

  • Il "fixed" limite di velocità dei criteri viene applicato a tutti i metodi di azione che non dispongono EnableRateLimiting di attributi e DisableRateLimiting .
  • Il "sliding" limite di velocità dei criteri viene applicato all'azione Privacy .
  • La limitazione della frequenza è disabilitata nel NoLimit metodo di azione.

Applicazione di attributi alle Razor pagine

Per Razor Pages, l'attributo deve essere applicato alla Razor pagina e non ai gestori di pagina. Ad esempio, [EnableRateLimiting] non può essere applicato a OnGet, OnPosto a qualsiasi altro gestore di pagine.

L'attributo DisableRateLimiting disabilita la limitazione della frequenza in una Razor pagina. EnableRateLimitingviene applicato a una Razor pagina solo se MapRazorPages().RequireRateLimiting(Policy) non è stato chiamato.

Confronto tra algoritmi limiter

I limiti fissi, scorrevoli e token limitano tutto il numero massimo di richieste in un periodo di tempo. Il limite di concorrenza limita solo il numero di richieste simultanee e non limita il numero di richieste in un periodo di tempo. Il costo di un endpoint deve essere considerato quando si seleziona un limiter. Il costo di un endpoint include le risorse usate, ad esempio tempo, accesso ai dati, CPU e I/O.

Esempi di limiter di frequenza

Gli esempi seguenti non sono destinati al codice di produzione, ma sono esempi su come usare i limiti.

Limiter con OnRejected, RetryAftere GlobalLimiter

L'esempio seguente:

  • Crea un callback RateLimiterOptions.OnRejected chiamato quando una richiesta supera il limite specificato. retryAfter può essere usato con TokenBucketRateLimiter, FixedWindowLimitere SlidingWindowLimiter perché questi algoritmi sono in grado di stimare quando verranno aggiunti altri permessi. Non ConcurrencyLimiter ha modo di calcolare quando saranno disponibili i permessi.

  • Aggiunge i limiti seguenti:

    • Oggetto SampleRateLimiterPolicy che implementa l'interfaccia IRateLimiterPolicy<TPartitionKey> . La SampleRateLimiterPolicy classe viene illustrata più avanti in questo articolo.
    • A SlidingWindowLimiter:
      • Con una partizione per ogni utente autenticato.
      • Una partizione condivisa per tutti gli utenti anonimi.
    • Oggetto GlobalLimiter applicato a tutte le richieste. Il limiter globale verrà eseguito per primo, seguito dal limiter specifico dell'endpoint, se presente. Crea GlobalLimiter una partizione per ogni IPAddressoggetto .
// 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();

Avviso

La creazione di partizioni negli indirizzi IP client rende l'app vulnerabile agli attacchi Denial of Service che usano lo spoofing degli indirizzi IP. Per altre informazioni, vedere BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service Attacks which employ IP Source Address Spoofing .For more information, see BCP 38 RFC 2827 Network Ingress filtering: Defeating Denial of Service Attacks which employ IP Source Address Spoofing.

Per il file completoProgram.cs, vedere il repository degli esempi.

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

Nel codice OnRejected precedente, OnRejectedContext usa per impostare lo stato della risposta su 429 Troppe richieste. Lo stato predefinito rifiutato è 503 Servizio non disponibile.

Limiter con autorizzazione

L'esempio seguente usa token JSWEB ON (JWT) e crea una partizione con il token di accesso JWT. In un'app di produzione, il token JWT viene in genere fornito da un server che funge da servizio token di sicurezza. Per lo sviluppo locale, lo strumento da riga di comando dotnet user-jwts può essere usato per creare e gestire JWT locali specifici dell'app.

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 con ConcurrencyLimiter, TokenBucketRateLimitere autorizzazione

L'esempio seguente:

  • Aggiunge un ConcurrencyLimiter oggetto con un nome di "get" criterio utilizzato nelle Razor pagine.
  • Aggiunge un TokenBucketRateLimiter oggetto con una partizione per ogni utente autorizzato e una partizione per tutti gli utenti anonimi.
  • Imposta RateLimiterOptions.RejectionStatusCode su 429 Troppe richieste.
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
            });
    }));

Per il file completoProgram.cs, vedere il repository degli esempi.

Test degli endpoint con limitazione della frequenza

Prima di distribuire un'app usando la limitazione della velocità di produzione, testare lo stress dell'app per convalidare i limiti di velocità e le opzioni usate. Ad esempio, creare uno script JMeter con uno strumento come BlazeMeter o Apache JMeter HTTP(S) Test Script Recorder e caricare lo script in Test di carico di Azure.

La creazione di partizioni con input utente rende l'app vulnerabile agli attacchi Denial of Service (DoS). Ad esempio, la creazione di partizioni negli indirizzi IP client rende l'app vulnerabile agli attacchi Denial of Service che usano lo spoofing degli indirizzi IP di origine. Per altre informazioni, vedere BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service Attacks that employ IP Source Address Spoofing .For more information, see BCP 38 RFC 2827 Network Ingress filtering: Defeating Denial of Service Attacks that employ IP Source Address Spoofing.

Risorse aggiuntive