مقالات › ASP.NET Core

آموزش Rate Limiting در ASP.NET Core؛ محافظت از API در برابر سوءاستفاده

استفاده از میان‌افزار داخلی محدودسازی نرخ در ASP.NET Core: الگوریتم‌های Fixed Window، Sliding Window، Token Bucket و Concurrency، محدودیت به‌ازای کاربر و IP، پاسخ 429 و نکات تولید پشت پراکسی.

API بدون محدودیت یک دعوت‌نامه‌ی باز است: یک کلاینت باگ‌دار در حلقه‌ی تلاش مجدد، یک اسکرپر یا مهاجمی که رمز عبور را Brute Force می‌کند، می‌تواند سرور و دیتابیس شما را از پا بیندازد. Rate Limiting (محدودسازی نرخ) تعداد درخواست‌هایی را که یک کلاینت در یک بازه‌ی زمانی می‌تواند بفرستد محدود می‌کند. از .NET 7 به بعد، ASP.NET Core یک میان‌افزار داخلی برای این کار دارد و به هیچ پکیج جانبی نیاز نیست.

چرا محدودسازی نرخ؟

  • محافظت از Endpointهای ورود و OTP در برابر Brute Force و هزینه‌ی سوءاستفاده از پیامک.
  • حفظ دسترس‌پذیری سرویس برای همه وقتی یک کلاینت بدرفتاری می‌کند.
  • کنترل هزینه‌ی عملیات سنگین (گزارش‌ها، فراخوانی هوش مصنوعی، پردازش فایل).
  • اعمال پلن‌ها — مثلاً پلن رایگان ۱۰۰ درخواست در ساعت و پلن پولی ۱۰٬۰۰۰.

چهار الگوریتم

محدودکننده نحوه‌ی کار مناسب برای
Fixed Window N درخواست در هر بازه‌ی ثابت (مثلاً ۱۰ درخواست در دقیقه، ریست در ابتدای دقیقه) محدودیت‌های ساده و عمومی
Sliding Window پنجره به چند بخش تقسیم می‌شود و انفجار درخواست در لبه‌ی پنجره را نرم می‌کند محدودیت عمومی منصفانه‌تر
Token Bucket سطل با نرخ ثابت پر می‌شود و هر درخواست یک توکن برمی‌دارد؛ انفجار کوتاه مجاز است APIهایی با کلاینت‌های پرنوسان
Concurrency حداکثر N درخواست هم‌زمان در حال اجرا Endpointهای سنگین و آپلود

راه‌اندازی پایه

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

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

    options.AddFixedWindowLimiter("fixed", o =>
    {
        o.PermitLimit = 100;
        o.Window = TimeSpan.FromMinutes(1);
        o.QueueLimit = 0;
    });
});

var app = builder.Build();

app.UseRateLimiter();   // بعد از Routing و قبل از Endpointها

app.MapGet("/api/products", GetProducts).RequireRateLimiting("fixed");

در کنترلرها از Attribute استفاده کنید:

[EnableRateLimiting("fixed")]
public class ProductsController : ControllerBase
{
    [HttpGet] public IActionResult Get() => Ok();

    [DisableRateLimiting]
    [HttpGet("health")] public IActionResult Health() => Ok();
}

هشدار: یک Policy نام‌دار مثل نمونه‌ی بالا بین همه‌ی کلاینت‌ها مشترک است — یعنی در مجموع ۱۰۰ درخواست در دقیقه، نه به‌ازای هر کاربر. برای APIهای واقعی تقریباً همیشه به محدودیت تفکیک‌شده (Partitioned) نیاز دارید.

تفکیک: به‌ازای IP یا کاربر

محدودکننده‌ی تفکیک‌شده به هر کلید (IP، شناسه‌ی کاربر، API Key) شمارنده‌ی جداگانه می‌دهد:

builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = 429;

    options.AddPolicy("per-user", httpContext =>
    {
        var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

        if (userId is not null)
            return RateLimitPartition.GetTokenBucketLimiter(userId, _ => new TokenBucketRateLimiterOptions
            {
                TokenLimit = 50,
                TokensPerPeriod = 10,
                ReplenishmentPeriod = TimeSpan.FromSeconds(10),
                QueueLimit = 0,
                AutoReplenishment = true,
            });

        var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        return RateLimitPartition.GetFixedWindowLimiter(ip, _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 20,
            Window = TimeSpan.FromMinutes(1),
        });
    });
});

کاربران احراز هویت‌شده سطل سخاوتمندانه‌ای می‌گیرند و ترافیک ناشناس به‌ازای IP محدود می‌شود.

Policy سخت‌گیرانه برای ورود و OTP

options.AddPolicy("auth", httpContext =>
    RateLimitPartition.GetSlidingWindowLimiter(
        httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        _ => new SlidingWindowRateLimiterOptions
        {
            PermitLimit = 5,
            Window = TimeSpan.FromMinutes(5),
            SegmentsPerWindow = 5,
        }));
[EnableRateLimiting("auth")]
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request) { ... }

پنج تلاش در هر پنج دقیقه به‌ازای هر IP، Brute Force را عملاً غیرممکن می‌کند. برای Endpointهای OTP به‌ازای شماره موبایل هم محدود کنید (از بدنه‌ی درخواست یا یک سرویس)؛ وگرنه مهاجم با چرخاندن IP همچنان می‌تواند یک قربانی را با پیامک بمباران کند.

محدودکننده‌ی سراسری

GlobalLimiter علاوه بر Policyهای هر Endpoint روی همه‌ی درخواست‌ها اعمال می‌شود و یک تور ایمنی خوب است:

options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
    RateLimitPartition.GetFixedWindowLimiter(
        ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        _ => new FixedWindowRateLimiterOptions { PermitLimit = 300, Window = TimeSpan.FromMinutes(1) }));

محدودیت هم‌زمانی برای کارهای سنگین

options.AddConcurrencyLimiter("reports", o =>
{
    o.PermitLimit = 4;          // حداکثر ۴ گزارش هم‌زمان
    o.QueueLimit = 10;          // تا ۱۰ درخواست دیگر در صف منتظر می‌مانند
    o.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
});

صف‌بندی، اوج ترافیک را به انتظار کوتاه تبدیل می‌کند نه خطا؛ صف را کوچک نگه دارید تا درخواست‌ها Timeout نشوند.

پاسخ 429 کاربرپسند

با هدر Retry-After و بدنه‌ی واضح به کلاینت بگویید کی دوباره تلاش کند:

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

    context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
    await context.HttpContext.Response.WriteAsJsonAsync(new ProblemDetails
    {
        Status = 429,
        Title = "Too many requests",
        Detail = "تعداد درخواست‌های شما بیش از حد مجاز است. کمی بعد دوباره تلاش کنید.",
    }, ct);

    var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
    logger.LogWarning("Rate limit hit for {Ip} on {Path}",
        context.HttpContext.Connection.RemoteIpAddress, context.HttpContext.Request.Path);
};

در کلاینت، به‌جای تلاش فوری، با پاسخ 429 به اندازه‌ی Retry-After صبر کنید (همراه با Exponential Backoff).

پشت پراکسی: گرفتن IP واقعی

پشت Nginx، IIS ARR، Cloudflare یا Load Balancer، مقدار RemoteIpAddress آدرس پراکسی است؛ یعنی همه‌ی بازدیدکنندگان یک Partition مشترک دارند. Forwarded Headers را قبل از Rate Limiter پیکربندی کنید:

builder.Services.Configure<ForwardedHeadersOptions>(o =>
{
    o.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    o.KnownProxies.Add(IPAddress.Parse("10.0.0.5"));   // فقط به پراکسی خودتان اعتماد کنید
});

app.UseForwardedHeaders();
app.UseRateLimiter();

هشدار: فقط از پراکسی‌های شناخته‌شده به X-Forwarded-For اعتماد کنید. وگرنه هر کلاینتی می‌تواند هدر را جعل کند و در هر درخواست محدودیت تازه بگیرد.

چند سرور

محدودکننده‌های داخلی شمارنده‌ها را در حافظه‌ی هر نمونه نگه می‌دارند. با سه سرور پشت Load Balancer، یک کلاینت عملاً سه برابر سهمیه دارد. این معمولاً قابل‌قبول است؛ اگر محدودیت سراسری دقیق لازم دارید، از ذخیره‌ساز توزیع‌شده (مثلاً محدودکننده‌های مبتنی بر Redis) استفاده کنید یا محدودیت را در Gateway اعمال کنید (limit_req در Nginx، Cloudflare، Azure API Management).

انتخاب اعداد

  • از ترافیک واقعی شروع کنید: صدک ۹۹ درخواست در دقیقه‌ی کاربران عادی را ببینید و محدودیت را با فاصله‌ی مناسب بالاتر بگذارید.
  • برای Endpointهای حساس (ورود، OTP، بازیابی رمز، فرم تماس) سخت‌گیرتر باشید.
  • ردشدن‌ها را لاگ و بررسی کنید؛ هم حملات را نشان می‌دهند و هم محدودیت‌های بیش از حد تنگ را.
  • اگر مصرف‌کنندگان API لازم دارند، اطلاعات سهمیه را در هدرها برگردانید (X-RateLimit-Remaining یک قرارداد رایج است).

جمع‌بندی

میان‌افزار داخلی ASP.NET Core الگوریتم‌های Fixed Window، Sliding Window، Token Bucket و Concurrency را پوشش می‌دهد. محدودیت‌ها را به‌ازای کاربر یا IP تفکیک کنید، برای احراز هویت و OTP سخت‌گیر باشید، یک محدودکننده‌ی سراسری به‌عنوان تور ایمنی اضافه کنید، پاسخ 429 مفید همراه با Retry-After برگردانید و پشت پراکسی Forwarded Headers را درست تنظیم کنید. چند خط پیکربندی هم از سرور و هم از کاربرانتان محافظت می‌کند.

پرسش‌های متداول

کدام محدودکننده را انتخاب کنم؟

Token Bucket برای محدودیت عمومی API به‌ازای کاربر (انفجار کوچک را مجاز می‌کند)، Sliding Window برای ورود و OTP، و Concurrency برای Endpointهای سنگین.

آیا Rate Limiting برای مقابله با DDoS کافی است؟

خیر. محدودیت در سطح برنامه در برابر سوءاستفاده و Brute Force محافظت می‌کند، اما حملات DDoS بزرگ باید زودتر متوقف شوند؛ در CDN، فایروال یا سطح ارائه‌دهنده.

آیا Rate Limiting با SignalR کار می‌کند؟

روی درخواست‌های HTTP مذاکره و اتصال اعمال می‌شود، نه روی تک‌تک پیام‌های Hub؛ فراخوانی متدهای Hub را داخل Hub یا با Hub Filter محدود کنید.