Application Security

حفاظت از دروازه‌ها: نگاهی عمیق به پیاده‌سازی مؤثر محدودیت نرخ

امنیت برنامه‌ها تنها به جلوگیری از دسترسی غیرمجاز محدود نمی‌شود؛ بلکه شامل اطمینان از دسترس‌پذیری و پایداری تحت بار نیز هست. یکی از حیاتی‌ترین دفاع‌ها در برابر سوءاستفاده، حملات brute-force و حملات Denial of Service (DoS)، محدودیت نرخ است. اگرچه بسیاری از توسعه‌دهندگان پیاده‌سازی‌های پایه‌ای را انجام می‌دهند، اما تنها تعداد کمی آن را با دقت و جزئیاتی که برای برنامه‌های سطح تولید لازم است، پیکربندی می‌کنند. در این پست، ما مکانیک‌های محدودیت نرخ، الگوریتم‌های محبوب و استراتژی‌های پیاده‌سازی عملی را بررسی خواهیم کرد.

چرا محدودیت نرخ اهمیت دارد؟

محدودیت نرخ، تعداد درخواست‌هایی را که یک کاربر یا آدرس IP می‌تواند در یک بازه زمانی مشخص به یک API ارسال کند، کنترل می‌کند. بدون این کنترل‌ها، عوامل مخرب می‌توانند به راحتی سرویس‌های شما را با بار بیش از حد مواجه کنند، منابع محاسباتی را مستهلک سازند یا یکپارچگی داده‌ها را از طریق تلاش‌های احراز هویت سریع و پیاپی به خطر بیندازند. فراتر از امنیت، محدودیت نرخ به عنوان ابزاری برای برنامه‌ریزی ظرفیت عمل می‌کند و اطمینان حاصل می‌کند که ترافیک مشروع، منابع سایر کاربران را تشنه نگذارد.

انتخاب الگوریتم مناسب

همه استراتژی‌های محدودیت نرخ یکسان نیستند. انتخاب الگوریتم به مورد استفاده خاص شما بستگی دارد، مانند اینکه آیا نیاز به اعمال محدودیت‌های سخت و دقیق دارید یا اجازه می‌دهید تا در برخی موارد، افزایش‌های ناگهانی ترافیک رخ دهد.

شمارنده پنجره ثابت

این ساده‌ترین رویکرد است. شما زمان را به پنجره‌های ثابت (مثلاً یک دقیقه) تقسیم می‌کنید و تعداد درخواست‌ها را در هر پنجره می‌شمارید. اگر تعداد از حد مجاز فراتر رود، درخواست‌های بعدی تا زمان بازنشانی پنجره رد می‌شوند.

نقص: این روش با «مشکل مرزی» مواجه است. اگر یک کاربر ۱۰۰ درخواست در پایان دقیقه اول و ۱۰۰ درخواست در آغاز دقیقه دوم ارسال کند، او در واقع ۲۰۰ درخواست را در عرض دو ثانیه ارسال کرده و از محدودیت هر دقیقه‌ای شما دور زده است.

لاگ پنجره لغزان

برای رفع مشکل مرزی، لاگ پنجره لغزان، زمان‌بندی (تایم‌استمپ) هر درخواست را ثبت می‌کند. وقتی درخواست جدیدی می‌رسد، سرور محاسبه می‌کند که چند درخواست در N ثانیه گذشته رخ داده است و آن را با حد مجاز مقایسه می‌کند.

نقص: این روش از نظر حافظه سنگین است. ذخیره زمان‌بندی هر درخواست برای میلیون‌ها کاربر، به فضای ذخیره‌سازی و قدرت پردازش قابل توجهی برای حذف کارآمد لاگ‌های قدیمی نیاز دارد.

الگوریتم سطل توکن

اغلب به عنوان استاندارد طلایی برای محدودیت نرخ با هدف عمومی در نظر گرفته می‌شود، الگوریتم سطل توکن اجازه می‌دهد تا افزایش‌های ناگهانی (Burst) رخ دهد، در حالی که میانگین بلندمدت را حفظ می‌کند. تصور کنید یک سطل با حداکثر ظرفیت وجود دارد. توکن‌ها با نرخ ثابت به سطل اضافه می‌شوند. هر درخواست یک توکن مصرف می‌کند. اگر سطل خالی باشد، درخواست رد می‌شود. این رویکرد انعطاف‌پذیر است زیرا به کاربر اجازه می‌دهد تا توکن‌ها را برای یک فعالیت ناگهانی «ذخیره» کند، به شرطی که سطل را در طول زمان پر کند.

پیاده‌سازی عملی با Redis

برای سیستم‌های توزیع‌شده، حفظ وضعیت به صورت محلی کافی نیست. شما به یک ذخیره‌سازی متمرکز و با عملکرد بالا مانند Redis نیاز دارید. در زیر یک پیاده‌سازی مفهومی با استفاده از الگوی شمارنده پنجره ثابت در Node.js آورده شده است که می‌توان آن را برای سایر زبان‌ها تطبیق داد.

const redis = require('redis');
const client = redis.createClient();

const RATE_LIMIT = 100; // حداکثر درخواست‌ها
const WINDOW_MS = 60000; // ۱ دقیقه

async function checkRateLimit(userId) {
    const key = `ratelimit:${userId}`;
    
    // دریافت شمارنده فعلی
    let count = await client.get(key);
    
    if (count === null || count === undefined) {
        // اولین درخواست در این پنجره، آن را تنظیم کنید
        await client.setex(key, WINDOW_MS / 1000, 1);
        return { allowed: true, remaining: RATE_LIMIT - 1 };
    }
    
    count = parseInt(count);
    
    if (count >= RATE_LIMIT) {
        return { allowed: false, remaining: 0 };
    }
    
    // افزایش شمارنده
    await client.incr(key);
    
    return { allowed: true, remaining: RATE_LIMIT - (count + 1) };
}

// استفاده در میان‌افزار Express
app.use(async (req, res, next) => {
    const userId = req.headers['x-user-id'] || req.ip;
    const result = await checkRateLimit(userId);
    
    if (!result.allowed) {
        return res.status(429).json({ error: 'Too Many Requests' });
    }
    
    // انتقال سهمیه باقی‌مانده به هدرها برای بازخورد به مشتری
    res.set('X-RateLimit-Remaining', result.remaining);
    next();
});

بهترین شیوه‌ها برای محیط تولید

پیاده‌سازی کد تنها نیمی از راه است. برای اطمینان از اینکه استراتژی محدودیت نرخ شما مؤثر است، موارد زیر را در نظر بگیرید:

  • هدرهای پاسخگو: همیشه هدرهای X-RateLimit-Limit، X-RateLimit-Remaining و Retry-After را شامل شوید. این شفافیت به توسعه‌دهندگان کمک می‌کند تا با API شما یکپارچه شوند و به مشتریان اطلاع می‌دهد که به حد مجاز رسیده‌اند.
  • کاهش عملکرد تدریجی (Graceful Degradation): اطمینان حاصل کنید که منطق محدودیت نرخ خود به یک گلوگاه تبدیل نمی‌شود. عملیات Redis باید غیرمسدودکننده (Non-blocking) یا به صورت ناهمگام (Asynchronous) انجام شوند تا از افزایش تأخیر جلوگیری شود.
  • محدودیت‌های لایه‌ای: محدودیت‌های مختلفی را بر اساس لایه‌های کاربر (مثلاً کاربران رایگان در مقابل کاربران ممتاز) یا نقاط پایانی API (مثلاً عملیات فقط خواندنی در مقابل عملیات نوشتنی) اعمال کنید.

نتیجه‌گیری

محدودیت نرخ یک جزء اساسی در امنیت برنامه است که هم از زیرساخت شما و هم از کاربران شما محافظت می‌کند. با درک ملاحظات و تعادل‌های بین الگوریتم‌های مختلف و بهره‌گیری از ابزارهای قدرتمندی مانند Redis، می‌توانید سیستمی بسازید که در برابر سوءاستفاده مقاوم باشد و در عین حال با ترافیک مشروع منصفانه رفتار کند. به خاطر داشته باشید، بهترین پیاده‌سازی امنیتی، آن است که تعادلی بین محافظت و تجربه کاربری ایجاد کند و اطمینان حاصل کند که API شما در تمام شرایط دسترس‌پذیر و پاسخگو باقی می‌ماند.

Share: