۱.۱. مفهوم PartitionedRateLimiter
درASP.NETCore 7 به بعد، مکانیزم Rate Limiting بهصورت بومی در فریمورک گنجانده شد. قلب این مکانیزم، کلاسPartitionedRateLimiterاست. ایدهٔ اصلی این است که بهجای یک محدودکنندهٔ سراسری (Global)، درخواستها را بر اساس یک «کلید» به پارتیشنهای مستقل تقسیم کنیم و برای هر پارتیشن یک سهمیهٔ جداگانه در نظر بگیریم.
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext =>
RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: ClientIpResolver.GetClientIpString(httpContext, behindCloudflare),
factory: _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 300,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6
}));
});
در این قطعهکد،partitionKeyتعیینکنندهٔ این است که هر درخواست به کدام «سطل» تعلق دارد. اگر دو درخواست کلید یکسان داشته باشند، سهمیهٔ مشترکی مصرف میکنند؛ اگر کلید متفاوت داشته باشند، از سهمیههای مستقل استفاده میکنند. بنابراینمنطق تعیین کلید، سرنوشت Rate Limiting را رقم میزند.
۱.۲. الگوریتم Sliding Window
الگوریتم پنجرهٔ لغزان، برخلاف پنجرهٔ ثابت (Fixed Window) که در آن شمارنده در مرزهای زمانی صفر میشود و میتواند منجر به «جهش» ترافیک در لحظهٔ تغییر پنجره شود، شمارش را در بازههای کوچکتر تقسیم میکند. در تنظیمات بالا:
در هر لحظه، مجموع درخواستهای ۶ سگمنت اخیر محاسبه میشود. اگر این مجموع ازPermitLimitفراتر رود، درخواستهای جدید رد میشوند. این الگوریتم توزیع نرمتری از نرخ درخواست ارائه میدهد، اما پیچیدگی محاسباتی بیشتری دارد و به همین دلیل، «کلید پارتیشن» اهمیت دوچندان پیدا میکند؛ زیرا هر پارتیشن ساختار دادهٔ مستقل خود را برای نگهداری سگمنتها نیاز دارد.
۱.۳. Middleware و ترتیب اجرا
برای اینکه Rate Limiter درست کار کند،app.UseRateLimiter()باید در جای مناسب Pipeline قرار گیرد:
app.UseForwardedHeaders(); // اول: اصلاح IP
app.UseRouting();
app.UseRateLimiter(); // بعد از Routing و ForwardedHeaders
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
اگر UseRateLimiter قبل از UseForwardedHeaders قرار گیرد، ممکن است IP واقعی هنوز اصلاح نشده باشد و کلید پارتیشن اشتباه محاسبه شود. این نکته در محیط آنلاین حیاتی است و در ادامه بیشتر توضیح میدهیم.
۲.۱. ماهیت Loopback
وقتی برنامه را روی سیستم شخصی خود اجرا میکنید و با مرورگر بهhttps://localhost:5001متصل میشوید، اتصال TCP از طریق رابط Loopback برقرار میشود. آدرس مبدأ در این اتصال، یکی از دو مقدار زیر است:
این بدان معناست کهHttpContext.Connection.RemoteIpAddressبرایتمام درخواستهامقدار یکسانی دارد. حتی اگر ده تب مختلف مرورگر باز کنید یا از چند ابزار مختلف (Postman، curl، مرورگر) استفاده کنید، همهٔ آنها از دید سرور «یک کاربر» هستند.
۲.۲. پیامد برای Partition Key
اگرClientIpResolver.GetClientIpStringدر حالت لوکال مقدار127.0.0.1را برگرداند، تمام درخواستها در یک پارتیشن واحد قرار میگیرند. حالا سهمیهٔ ۳۰۰ درخواست در دقیقه برایکل برنامهمصرف میشود، نه برای هر کاربر.
۲.۳. درخواستهای پنهان که شمارش میشوند
نکتهٔ بسیار مهمی که اغلب نادیده گرفته میشود این است که Rate Limiterهمهٔ درخواستهای HTTPرا شمارش میکند، نه فقط درخواستهای API. بارگذاری یک صفحهٔ وب مدرن شامل موارد زیر است:
در محیط Development، به دلیل فعال بودن Hot Reload و ابزارهای Debugging، ممکن است درخواستهای بیشتری نیز ارسال شود (مثلاً WebSocket برای Hot Reload، درخواستهای پایش فایل). بنابراین یک بارگذاری سادهٔ صفحه میتواند بهراحتی ۲۰ تا ۵۰ درخواست تولید کند. اگرPermitLimitدر فایلappsettings.Development.jsonبه عددی پایین مثل ۷ یا ۸ تنظیم شده باشد، در همان لحظهٔ اول بلاک میشوید.
۲.۴. احتمال Override شدن تنظیمات
بسیاری از تیمها مقادیر متفاوتی برای محیطهای مختلف درappsettings.{Environment}.jsonتعریف میکنند. ممکن است درappsettings.Development.jsonمقدارPermitLimitبه عددی بسیار کوچک تنظیم شده باشد تا توسعهدهنده بتواند رفتار Rate Limiting را در حالت Development تست کند. این تنظیم در محیط Production اعمال نمیشود و همین باعث تفاوت رفتار میشود.
۳.۱. زنجیرهٔ پروکسی
در محیط آنلاین، معمولاً معماری به این صورت است:
[مرورگر کاربر] → [لبهٔ Cloudflare] → [سرور مبدأ (Origin)]
اتصال TCP از دید سرور مبدأ، از سمتسرورهای Cloudflareباز میشود، نه از سمت مرورگر کاربر. بنابراینRemoteIpAddressدر سرور شما، یکی از IPهای شبکهٔ Cloudflare است (مثلاً172.68.x.xیا162.158.x.x). اگر برنامه مستقیماً به این مقدار اعتماد کند، همهٔ کاربرانی که از طریق یک لبهٔ خاص Cloudflare متصل شدهاند، در یک پارتیشن مشترک قرار میگیرند — که این سناریو در واقع بدتر از لوکال است، زیرا ممکن است هزاران کاربر همزمان در یک پارتیشن جمع شوند.
۳.۲. نقش هدرهای Forwarded
برای حل این مشکل، Cloudflare آدرس IP واقعی کاربر را در هدرهای HTTP قرار میدهد:
هنگامی کهBehindCloudflare = trueاست،ClientIpResolverباید این هدرها را بخواند. بنابراین کلید پارتیشن برای هر کاربر،IP عمومی واقعی اوخواهد بود. هر کاربر سهمیهٔ مستقل خود را دارد و طبیعی است که یک کاربر عادی هرگز به ۳۰۰ درخواست در دقیقه نمیرسد.
۳.۳. اعتماد به هدرها و خطر جعل
نکتهٔ امنیتی مهم این است که هدرهایX-Forwarded-ForوCF-Connecting-IPتنها در صورتی قابل اعتماد هستند که:
اگر مهاجم بتواند مستقیماً به سرور مبدأ متصل شود و هدرCF-Connecting-IPرا جعل کند، میتواند پارتیشن دلخواه خود را انتخاب کند و Rate Limiter را دور بزند. بنابراین، فایروال سرور مبدأ باید فقط ترافیک IPهای Cloudflare را بپذیرد.
۳.۴. مثال پیادهسازی ClientIpResolver
یک پیادهسازی ایمن از استخراج IP میتواند به این شکل باشد:
public static class ClientIpResolver
{
public static string GetClientIpString(HttpContext context, bool behindCloudflare)
{
if (behindCloudflare)
{
var cfIp = context.Request.Headers["CF-Connecting-IP"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(cfIp) && IPAddress.TryParse(cfIp, out _))
return cfIp;
var xff = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(xff))
{
var first = xff.Split(',')[0].Trim();
if (IPAddress.TryParse(first, out _))
return first;
}
}
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
}
نکته: همیشه قبل از اعتماد به مقدار هدر، آن را با IPAddress.TryParse اعتبارسنجی کنید تا از تزریق مقادیر نامعتبر جلوگیری شود.
۴.۱. NAT و IPهای مشترک
در محیط واقعی، IPها همیشه یکتا نیستند. دهها یا صدها کاربر میتوانند از پشت یک NAT (مثلاً شبکهٔ یک شرکت یا اپراتور تلفن همراه) با یک IP عمومی مشترک به سایت شما متصل شوند. در این حالت، حتی در محیط آنلاین، چندین کاربر در یک پارتیشن قرار میگیرند. اگر حجم ترافیک یک شرکت بزرگ باشد، ممکن است بهطور ناخواسته به سقف ۳۰۰ درخواست در دقیقه برسند و همه با هم بلاک شوند. این پدیده به نام «Collateral Blocking» یا «بلاک جانبی» شناخته میشود.
راهحلهای رایج:
۴.۲. تفاوت IPv4 و IPv6
در محیط لوکال، اگر مرورگر از IPv6 استفاده کند،::1را میبینید و اگر از IPv4 استفاده کند،127.0.0.1. این دو کلید پارتیشن متفاوت هستند. بنابراین اگر نیمی از درخواستها از IPv6 و نیمی از IPv4 بیایند، شما عملاًدوپارتیشن با سهمیهٔ ۳۰۰ دارید (مجموعاً ۶۰۰). این نکته در محیط لوکال میتواند منجر به رفتار گیجکننده شود.
در محیط آنلاین نیز همین موضوع برقرار است: برخی کاربران ممکن است با IPv6 و برخی دیگر با IPv4 متصل شوند. اگر میخواهید کاربرانی که هم IPv4 و هم IPv6 دارند در یک پارتیشن قرار بگیرند، باید IP را نرمالسازی کنید (مثلاً با استفاده از کتابخانههایی که IPv6 را به فرمت کانونیک تبدیل میکنند) یا از شناسهٔ کاربر احراز هویتشده استفاده کنید.
۴.۳. استفاده از ForwardedHeadersMiddleware
در ASP.NET Core، Middleware استاندارد ForwardedHeadersوجود دارد که بهطور خودکارRemoteIpAddressرا بر اساس هدرX-Forwarded-Forاصلاح میکند. اما این Middleware بهطور پیشفرض به هیچ پروکسی اعتماد نمیکند و باید صریحاً پیکربندی شود:
builder.Services.Configure(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
// یا صریحاً شبکههای Cloudflare را اضافه کنید
});
توجه داشته باشید که پاک کردن KnownNetworks و KnownProxies و اعتماد به همهٔ IPها خطرناک است و تنها در صورتی قابل قبول است که سرور مبدأ پشت فایروال و فقط برای IPهای Cloudflare قابل دسترسی باشد.
یک تصور غلط رایج این است که «در آنلاین Rate Limiter کار نمیکند». در واقعیت، Rate Limiter در آنلاین هم فعال است و کار میکند، اما به دلیل توزیع درخواستها بین پارتیشنهای متعدد (به ازای هر IP کاربر)، احتمال برخورد یک کاربر با سقف بسیار کم است. برای اینکه این موضوع را اثبات کنید، کافی است از یک IP ثابت (مثلاً یک سرور یا اسکریپت) با ابزاری مثلwrkیاheyبیش از ۳۰۰ درخواست در دقیقه به سایت بزنید. مشاهده خواهید کرد که پس از رسیدن به سقف، پاسخهای ۴۲۹ دریافت میکنید.
بنابراین، تفاوت بین لوکال و آنلاین «وجود یا عدم وجود» Rate Limiting نیست، بلکهتوزیع بار بین پارتیشنهااست. در لوکال، تمام بار روی یک پارتیشن میافتد و سقف سریع پر میشود؛ در آنلاین، بار بین هزاران پارتیشن تقسیم میشود و هر پارتیشن بهندرت به سقف میرسد.
۶.۱. غیرفعالسازی در Development
سادهترین و رایجترین راهکار، غیرفعال کردن Rate Limiter در محیط Development است:
if (!builder.Environment.IsDevelopment())
{
builder.Services.AddRateLimiter(options => { ... });
app.UseRateLimiter();
}
مزیت: تجربهٔ توسعهٔ روان، بدون مزاحمتهای ناخواسته. عیب: توسعهدهنده ممکن است رفتار Rate Limiting را در محیط لوکال تست نکند و باگهای مربوط به آن در Production کشف شوند.
۶.۲. استفاده از سقف بالاتر در Development
راهکار دیگر، افزایشPermitLimitدرappsettings.Development.jsonاست:
{
"RateLimiting": {
"PermitLimit": 10000,
"WindowMinutes": 1
}
}
این روش اجازه میدهد Middleware فعال بماند و در صورت بروز خطا، در محیط لوکال قابل ردیابی باشد، بدون اینکه توسعهدهنده را آزار دهد.
۶.۳. استفاده از کلید پارتیشن متفاوت در Development
اگر میخواهید مکانیزم Rate Limiting را در لوکال تست کنید اما نمیخواهید همهٔ درخواستها یک پارتیشن داشته باشند، میتوانید در محیط Development از یک شناسهٔ تصادفی بهعنوان کلید استفاده کنید:
partitionKey: builder.Environment.IsDevelopment()
? Guid.NewGuid().ToString()
: ClientIpResolver.GetClientIpString(httpContext, behindCloudflare)
توجه: این کار رفتار واقعی را شبیهسازی نمیکند و صرفاً برای تست اجزای دیگر مناسب است.
۶.۴. شبیهسازی Cloudflare در لوکال
برای تست سناریوی واقعی، میتوانید یک هدرCF-Connecting-IPمصنوعی در درخواستهای لوکال تنظیم کنید (مثلاً از طریق Postman یا یک Middleware تست). سپسClientIpResolverرا طوری تنظیم کنید که در Development هم این هدر را بخواند. این روش به شما اجازه میدهد سناریوی چندکاربره را در محیط لوکال شبیهسازی کنید.
۶.۵. پیکربندی صحیح ForwardedHeaders
options.OnRejected = async (context, token) =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService>();
var ip = ClientIpResolver.GetClientIpString(context.HttpContext, behindCloudflare);
logger.LogWarning("Rate limit exceeded for IP: {Ip}, Path: {Path}",
ip, context.HttpContext.Request.Path);
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsync("Too Many Requests");
};
مطمئن شوید کهUseForwardedHeadersقبل ازUseRateLimiterقرار دارد و بهدرستی پیکربندی شده است:
app.UseForwardedHeaders();
app.UseRouting();
app.UseRateLimiter();
همچنین در محیط آنلاین، فقط به IPهای Cloudflare اعتماد کنید و پورتهای سرور مبدأ را برای عموم ببندید.
۶.۶. پایش و Logging
برای اینکه درک بهتری از رفتار Rate Limiter داشته باشید، رخدادهای بلاک شدن را لاگ کنید:
این لاگها در محیط Production نشان میدهند که آیا Rate Limiter واقعاً فعال است و کدام IPها بلاک میشوند.
۷.۱. جعل هدر و بایپس Rate Limiting
اگر سرور مبدأ مستقیماً از اینترنت قابل دسترسی باشد و مهاجم بتواند هدرCF-Connecting-IPرا جعل کند، میتواند Rate Limiter را دور بزند. بنابراین:
پورتهای سرور مبدأ باید فقط برای IPهای Cloudflare باز باشند.
از ابزارهایی مانندufwیاiptablesبرای محدودسازی استفاده کنید.
در صورت امکان از Tunnel (Cloudflare Tunnel) استفاده کنید تا سرور مبدأ هیچ پورت عمومی نداشته باشد.
۷.۲. حملهٔ توزیعشده (DDoS)
Rate Limiting مبتنی بر IP در برابر حملات DDoS توزیعشده (که از هزاران IP مختلف انجام میشود) کارایی محدودی دارد، زیرا هر IP فقط چند درخواست میفرستد و از سقف عبور نمیکند. در چنین مواردی باید از مکانیزمهای لایهٔ بالاتر مانند Cloudflare WAF، Bot Management، و Turnstile (که در تنظیمات شما نیز دیده میشود) استفاده کنید.
۷.۳. ترکیب Rate Limiting با CAPTCHA
در تنظیمات شما Cloudflare.Turnstile و Google.Recaptchaدیده میشود. ترکیب Rate Limiting با CAPTCHA میتواند تجربهٔ امنیتی بهتری ارائه دهد: بهجای بلاک کردن کامل، به کاربر امکان حل CAPTCHA داده شود. این رویکرد بهویژه برای کاربران پشت NAT مشترک مفید است.
با نگاه به تنظیمات موجود، چند نکته قابل توجه است:
BehindCloudflare = true: این پرچم باید در محیط Production فعال و در محیط Development غیرفعال باشد، یا حداقل منطقClientIpResolverباید در Development از هدرهایCF-Connecting-IPصرفنظر کند.
KingCaptcha = true: این پرچم نشان میدهد که CAPTCHA فعال است. این میتواند در کاهش بار روی Rate Limiter مؤثر باشد، اما جایگزین آن نمیشود.
سقف ۳۰۰ درخواست در دقیقه: این عدد برای یک کاربر عادی بسیار بالاست و عملاً فقط جلوی رباتها و اسکریپتهای ساده را میگیرد. اگر نگران حملات هدفمندتر هستید، باید سقفهای اختصاصی برای Endpointهای حساس (مانند ورود، ثبتنام، فراموشی رمز) تعریف کنید.
عدم وجود Rate Limiting اختصاصی برای Endpoint: GlobalLimiterبرای همهٔ درخواستها اعمال میشود. بهتر است برای مسیرهای حساس ازRequireRateLimitingبا Policyهای جداگانه استفاده کنید:
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("login", context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: ClientIpResolver.GetClientIpString(context, behindCloudflare),
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(5)
}));
});
app.MapPost("/api/auth/login", ...).RequireRateLimiting("login");
تفاوت رفتار Rate Limiter بین محیط لوکال و آنلاین، نه یک باگ، بلکه نتیجهٔ منطقی نحوهٔ تعیین «کلید پارتیشن» و ماهیت متفاوت آدرس IP در این دو محیط است. در لوکال، همهٔ درخواستها از127.0.0.1یا::1میآیند و در یک پارتیشن جمع میشوند؛ در آنلاین پشت Cloudflare، هر کاربر IP عمومی خود را دارد و در پارتیشن جداگانه قرار میگیرد.
توصیههای عملی:
درک دقیق این مفاهیم نهتنها از سردرگمی در محیط توسعه جلوگیری میکند، بلکه به شما امکان میدهد Rate Limiting را بهصورت هدفمند و امن در محیط عملیاتی پیادهسازی کنید و از منابع سرور در برابر سوءاستفاده محافظت نمایید.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.