← tous les articles

.NET

Implémenter le rate limiting dans les API ASP.NET Core

Découvrez comment protéger les API ASP.NET Core avec le middleware de rate limiting intégré, les named policies, le partitionnement par client, les responses HTTP 429 et des choix de configuration pratiques.

Pipeline de requests d’une API ASP.NET Core avec un middleware de rate limiting rejetant les requests excédentaires avec HTTP 429

Le rate limiting contrôle le nombre de requests qu’une API accepte pendant une période donnée. Il est utile pour les API publiques, les endpoints d’authentification, les opérations coûteuses et les intégrations dans lesquelles un client ne doit pas consommer toutes les ressources disponibles.

ASP.NET Core fournit un middleware de rate limiting intégré avec les algorithmes fixed window, sliding window, token bucket et concurrency. Les policies peuvent être globales ou attribuées uniquement à certains endpoints. (learn.microsoft.com)

Pourquoi le rate limiting est important

Un rate limiter peut aider une API à :

  • empêcher un seul client de monopoliser les ressources ;
  • réduire les pics de trafic accidentels ;
  • protéger les capacités de la database, du CPU et des services externes ;
  • appliquer des limites différentes selon les utilisateurs ou les offres ;
  • renvoyer une response prévisible lorsque la capacité n’est pas disponible.

Le rate limiting ne constitue pas une protection DDoS complète. Les attaques distribuées de grande ampleur doivent également être traitées aux frontières de l’infrastructure, par exemple au moyen d’un CDN, d’un pare-feu d’application web, d’une passerelle API ou d’un service de protection cloud. (learn.microsoft.com)

Configurer une policy fixed window

L’algorithme fixed window autorise un nombre défini de requests pendant une fenêtre temporelle. Lorsque la fenêtre se termine, son compteur est réinitialisé.

La configuration Program.cs suivante accepte 20 requests par minute et place en file d’attente jusqu’à deux requests supplémentaires :

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    options.AddFixedWindowLimiter("api", limiterOptions =>
    {
        limiterOptions.PermitLimit = 20;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.QueueLimit = 2;
        limiterOptions.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;
    });
});

var app = builder.Build();

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

app.MapGet("/products", () => Results.Ok(new[]
{
    new { Id = 1, Name = "Keyboard" },
    new { Id = 2, Name = "Mouse" }
})).RequireRateLimiting("api");

app.Run();

PermitLimit définit le nombre de requests acceptées, tandis que Window définit le moment où les autorisations sont renouvelées. QueueLimit contrôle le nombre de requests pouvant attendre une autorisation. Une valeur de zéro rejette immédiatement les requests excédentaires.

Pour les policies propres à un endpoint, UseRateLimiter doit s’exécuter après UseRouting. (learn.microsoft.com)

Renvoyer une response 429 utile

Par défaut, les requests rejetées peuvent recevoir le code d’état HTTP 429 Too Many Requests. Un callback OnRejected permet à l’API de renvoyer une response plus claire et, lorsqu’il est disponible, un en-tête Retry-After.

builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    options.OnRejected = async (context, cancellationToken) =>
    {
        if (context.Lease.TryGetMetadata(
            MetadataName.RetryAfter,
            out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int)retryAfter.TotalSeconds).ToString();
        }

        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            error = "Too many requests. Please try again later."
        }, cancellationToken);
    };

    options.AddFixedWindowLimiter("api", limiterOptions =>
    {
        limiterOptions.PermitLimit = 20;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.QueueLimit = 0;
    });
});

La valeur Retry-After aide les clients à déterminer quand une nouvelle request a des chances d’aboutir. Il reste important que les clients gèrent les responses 429 au lieu de supposer que chaque request sera acceptée.

Limiter chaque client séparément

Avec une seule policy partagée, tous les appelants consomment le même quota de requests. Le partitionnement crée un limiter distinct pour chaque utilisateur, clé API, locataire ou adresse IP.

options.AddPolicy("per-client", httpContext =>
{
    var clientId =
        httpContext.User.Identity?.Name ??
        httpContext.Connection.RemoteIpAddress?.ToString() ??
        "anonymous";

    return RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: clientId,
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 10,
            Window = TimeSpan.FromMinutes(1),
            QueueLimit = 0,
            AutoReplenishment = true
        });
});

Appliquez-la comme n’importe quelle autre named policy :

app.MapPost("/orders", CreateOrder)
   .RequireRateLimiting("per-client");

Un ID utilisateur authentifié ou une clé API contrôlée constitue généralement une clé de partition plus fiable qu’une adresse IP. Les applications placées derrière des proxies doivent également configurer correctement les en-têtes transférés avant de s’appuyer sur l’adresse IP du client.

Choisir un algorithme

Utilisez fixed window lorsqu’un quota simple suffit. Utilisez sliding window pour obtenir des limites plus régulières aux frontières des fenêtres. Utilisez token bucket lorsque des pics contrôlés sont acceptables. Utilisez concurrency limiting lorsque la principale préoccupation est le nombre d’opérations coûteuses exécutées simultanément, plutôt que le nombre de requests reçues par minute. (learn.microsoft.com)

Avant le déploiement en production, conservez les limites dans la configuration, surveillez les requests rejetées et effectuez des tests de charge avec les valeurs sélectionnées. Une limite doit refléter le coût et la capacité réels de l’endpoint protégé, et non un nombre arbitraire recopié dans toute l’API.

Références