Partager via


Intergiciel de réécriture d’URL dans ASP.NET Core

Par Arvin Kahbazi, Maarten Balliauw et Rick Anderson

L’intergiciel Microsoft.AspNetCore.RateLimiting fournit un intergiciel de limitation de débit. Les applications configurent des stratégies de limitation du débit, puis attachent les stratégies aux points de terminaison. Les applications utilisant la limitation de débit doivent être soigneusement testées et examinées avant le déploiement. Pour plus d’informations, consultez Test des points de terminaison avec limitation de débit.

Pour obtenir une introduction à la limitation de débit, consultez Intergiciel de limitation de débit.

Algorithmes de limiteur de débit

La classe RateLimiterOptionsExtensions fournit les méthodes d’extension suivantes pour la limitation du débit :

Limiteur de fenêtre fixe

La méthode AddFixedWindowLimiter utilise une fenêtre de temps fixe pour limiter les requêtes. Lorsque la fenêtre de temps expire, une nouvelle fenêtre de temps démarre et la limite de requête est réinitialisée.

Prenez le code suivant :

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

Le code précédent :

  • Appelle AddRateLimiter pour ajouter un service de limitation de débit à la collection de services.
  • Appelle AddFixedWindowLimiter pour créer un limiteur de fenêtre fixe avec un nom de stratégie de "fixed" et définit :
  • PermitLimit sur 4 et le temps Window sur 12. Un maximum de 4 requêtes pour chaque fenêtre de 12 secondes est autorisé.
  • QueueProcessingOrder en OldestFirst.
  • QueueLimit sur 2.
  • Appelle UseRateLimiter pour activer la limitation du débit.

Les applications doivent utiliser Configuration pour définir les options de limiteur. Le code suivant met à jour le code précédent à l’aide de MyRateLimitOptions pour la configuration :

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 doit être appelé après UseRouting lorsque des API spécifiques au point de terminaison de limitation de débit sont utilisées. Par exemple, si l’attribut [EnableRateLimiting] est utilisé, UseRateLimiter doit être appelé après UseRouting. Lorsque vous appelez uniquement des limiteurs globaux, UseRateLimiter peut être appelé avant UseRouting.

Limiteur à fenêtre glissante

Un algorithme à fenêtre glissante :

  • Est similaire au limiteur de fenêtre fixe, mais avec des segments par fenêtre. La fenêtre fait glisser un segment par intervalle de segment. L’intervalle de segment est (heure de la fenêtre)/(segments par fenêtre).
  • Limite les requêtes d’une fenêtre à permitLimit requêtes.
  • Chaque fenêtre de temps est divisée en n segments par fenêtre.
  • Les requêtes extraites du segment de temps expiré d’une fenêtre plus tôt (n segments avant le segment actuel) sont ajoutées au segment actuel. Nous faisons référence au segment de temps le plus expiré d’une fenêtre en arrière en tant que segment expiré.

Considérez le tableau suivant qui montre un limiteur à fenêtre glissante avec une fenêtre de 30 secondes, trois segments par fenêtre et une limite de 100 requêtes :

  • La ligne supérieure et la première colonne affichent le segment de temps.
  • La deuxième ligne indique les requêtes restantes disponibles. Les requêtes restantes sont calculées en soustrayant aux demandes disponibles les demandes traitées et en ajoutant les demandes recyclées.
  • Les requêtes se déplacent à chaque fois le long de la ligne bleue diagonale.
  • À partir de l’heure 30, la requête issue du segment de temps expiré est ajoutée à la limite de requête, comme indiqué dans les lignes rouges.

Tableau montrant les requêtes, les limites et les emplacements recyclés

Le tableau suivant montre les données du graphique précédent dans un autre format. La colonne Disponible affiche les requêtes disponibles à partir du segment précédent (Report de la ligne précédente). La première ligne indique 100 requêtes disponibles, car il n’y a pas de segment précédent.

Temps Disponible Pris Recyclé à partir des expirées Report
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

Le code suivant utilise le limiteur de débit à fenêtre glissante :

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

Limiteur de compartiment de jetons

Le limiteur de compartiment de jetons est similaire au limiteur à fenêtre glissante, mais au lieu d’ajouter en arrière les requêtes prises à partir du segment expiré, un nombre fixe de jetons est ajouté à chaque période de réapprovisionnement. Les jetons ajoutés à chaque segment ne peuvent pas augmenter les jetons disponibles à un nombre supérieur à la limite du compartiment de jetons. Le tableau suivant montre un limiteur de compartiment de jetons avec une limite de 100 jetons et une période de réapprovisionnement de 10 secondes.

Temps Disponible Pris Ajouté(e) Report
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

Le code suivant utilise le limiteur de compartiment de jetons :

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

Quand AutoReplenishment est défini sur true, un minuteur interne réapprovisionne les jetons tous les ReplenishmentPeriod. Quand il est défini sur false, l’application doit appeler TryReplenish sur le limiteur.

Limiteur d’accès concurrentiel

Le limiteur d’accès concurrentiel limite le nombre de requêtes simultanées. Chaque requête réduit la limite d’accès concurrentiel de un. Lorsqu’une requête se termine, la limite est augmentée de un. Contrairement aux autres limiteurs de requêtes qui limitent le nombre total de requêtes pour une période spécifiée, le limiteur d’accès concurrentiel limite uniquement le nombre de requêtes simultanées et ne limite pas le nombre de requêtes dans une période donnée.

Le code suivant utilise le limiteur d’accès concurrentiel :

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

Créer des limiteurs chaînés

L’API CreateChained permet de passer plusieurs PartitionedRateLimiter qui sont combinés en un seul PartitionedRateLimiter. Le limiteur combiné exécute tous les limiteurs d’entrée dans l’ordre.

Le code suivant utilise 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();

Pour plus d’informations, consultez le code source CreateChained

Attributs EnableRateLimiting et DisableRateLimiting

Les attributs [EnableRateLimiting] et [DisableRateLimiting] peuvent être appliqués à un contrôleur, à une méthode d’action ou à une page Razor. Pour les pages Razor, l’attribut doit être appliqué à la page Razor et non aux gestionnaires de pages. Par exemple, [EnableRateLimiting] ne peut pas être appliqué à OnGet, OnPost ou à tout autre gestionnaire de page.

L’attribut [DisableRateLimiting] désactive la limitation du débit du contrôleur, de la méthode d’action ou de la page Razor, quels que soient les limiteurs de débit nommés ou les limites globales appliquées. Par exemple, considérez le code suivant qui appelle RequireRateLimiting pour appliquer la limitation de débit fixedPolicy à tous les points de terminaison de contrôleur :

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

Dans le code suivant, [DisableRateLimiting] désactive la limitation du débit et les remplacements appliqués par [EnableRateLimiting("fixed")] au Home2Controller et app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) appelés dans 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 });
    }
}

Dans le code précédent, le [EnableRateLimiting("sliding")] n’est pas appliqué à la méthode d’action Privacy, car Program.cs a appelé app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy).

Considérez le code suivant qui n’appelle pas RequireRateLimiting sur MapRazorPages ou 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();

Examinons le contrôleur ci-dessous :

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

Dans le contrôleur précédent :

  • Le limiteur de taux de stratégie "fixed" est appliqué à toutes les méthodes d’action qui n’ont pas les attributs EnableRateLimiting et DisableRateLimiting.
  • Le limiteur de taux de stratégie "sliding" est appliqué à l’action Privacy.
  • La limitation du débit est désactivée sur la méthode d’action NoLimit.

Application d'attributs à des pages Razor

Pour les pages Razor, l’attribut doit être appliqué à la page Razor et non aux gestionnaires de pages. Par exemple, [EnableRateLimiting] ne peut pas être appliqué à OnGet, OnPost ou à tout autre gestionnaire de page.

L’attribut DisableRateLimiting désactive la limitation de débit sur une page Razor. EnableRateLimiting est appliqué uniquement à une page Razor si MapRazorPages().RequireRateLimiting(Policy) n’a pas été appelé.

Comparaison d’algorithmes de limiteur

Les limiteurs fixes, glissants et de jetons limitent tous le nombre maximal de requêtes dans une période donnée. Le limiteur d’accès concurrentiel limite uniquement le nombre de requêtes simultanées et ne limite pas le nombre de requêtes dans une période donnée. Le coût d’un point de terminaison doit être pris en compte lors de la sélection d’un limiteur. Le coût d’un point de terminaison inclut les ressources utilisées, par exemple, le temps, l’accès aux données, le processeur et les E/S.

Exemples de limiteur de débit

Les exemples suivants ne sont pas destinés au code de production, mais sont des exemples sur l’utilisation des limiteurs.

Limiteur avec OnRejected, RetryAfter et GlobalLimiter

L’exemple suivant :

  • Crée un rappel RateLimiterOptions.OnRejected qui est appelé lorsqu’une requête dépasse la limite spécifiée. retryAfter peut être utilisé avec TokenBucketRateLimiter, FixedWindowLimiter et SlidingWindowLimiter, car ces algorithmes sont en mesure d’estimer quand d’autres autorisations seront ajoutées. ConcurrencyLimiter ne dispose d’aucun moyen de calculer le moment où les permis seront disponibles.

  • Ajoute les limiteurs suivants :

    • SampleRateLimiterPolicy qui implémente l'interface IRateLimiterPolicy<TPartitionKey>. La classe SampleRateLimiterPolicy est montrée plus bas dans cet article.
    • Une SlidingWindowLimiter :
      • Avec une partition pour chaque utilisateur authentifié.
      • Une partition partagée pour tous les utilisateurs anonymes.
    • GlobalLimiter appliqué à toutes les requêtes. Le limiteur global est exécuté en premier, suivi du limiteur spécifique au point de terminaison, le cas échéant. GlobalLimiter crée une partition pour chaque IPAddress.
// Preceding code removed for brevity.
using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

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

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

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

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

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

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

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

        return new ValueTask();
    };

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

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

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

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

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

var app = builder.Build();

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

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

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

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

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

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

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

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

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

app.Run();

Avertissement

La création de partitions sur des adresses IP clientes rend l’application vulnérable aux attaques par déni de service qui utilisent l’usurpation d’adresses IP source. Pour plus d’informations, consultez Filtrage d’entrée réseau BCP 38 RFC 2827 : Contrer les attaques par déni de service (DoS) qui utilisent l’usurpation d’adresses IP Source.

Consultez le référentiel d’exemples pour connaître le fichier Program.cs complet.

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

Dans le code précédent, OnRejected utilise OnRejectedContext pour définir l’état de réponse sur 429 Nombre de requêtes trop élevé. La valeur par défaut de l’état rejeté est 503 Service indisponible.

Limiteur avec autorisation

L’exemple suivant utilise des jetons web JSON (JWT) et crée une partition avec le jeton d’accès JWT. Dans une application de production, le JWT est généralement fourni par un serveur agissant en tant que service de jeton de sécurité (STS). Pour le développement local, l’outil en ligne de commande user-jwts dotnet peut être utilisé pour créer et gérer des JWT locaux spécifiques à l’application.

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

Limiteur avec ConcurrencyLimiter, TokenBucketRateLimiter et autorisation

L’exemple suivant :

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

Consultez le référentiel d’exemples pour connaître le fichier Program.cs complet.

Test des points de terminaison avec limitation de débit

Avant de déployer une application à l’aide de la limitation de débit en production, effectuez un test de contrainte de l’application pour valider les limites de débit et les options utilisées. Par exemple, créez un script JMeter avec un outil tel que BlazeMeter ou Apache JMeter HTTP(S) Test Script Recorder et chargez le script dans Test de charge Azure.

La création de partitions avec une entrée utilisateur rend l’application vulnérable aux attaques par déni de service (DoS). Par exemple, la création de partitions sur des adresses IP clientes rend l’application vulnérable aux attaques par déni de service qui utilisent l’usurpation d’adresses IP source. Pour plus d’informations, consultez Filtrage d’entrée réseau BCP 38 RFC 2827 : Contrer les attaques par déni de service (DoS) qui utilisent l’usurpation d’adresses IP Source.

Ressources supplémentaires