Application Security

پیاده‌سازی محدودیت نرخ توزیع‌شده با Redis و اسکریپت‌های Lua برای APIهای با در دسترس‌بودن بالا

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

چالش اتمیک بودن توزیع‌شده

تصور کنید سه سرور API پشت یک توزیع‌کننده بار (Load Balancer) دارید. یک کاربر مخرب ۱۰۰ درخواست در ثانیه ارسال می‌کند و به صورت چرخشی به سرورهای A، B و C حمله می‌کند. اگر شمارنده‌های محدودیت نرخ را به صورت محلی روی هر سرور ذخیره کنید، کاربر عملاً ۳۰۰ درخواست در ثانیه مجاز خواهد داشت و محدودیت ۱۰۰ درخواست در ثانیه شما را دور می‌زند. شما به یک شمارنده متمرکز نیاز دارید.

به نظر می‌رسد که صرفاً افزایش یک کلید در Redis (مثلاً با INCR) راه‌حلی باشد، اما این روش بدون منطق پنجره‌بندی مناسب در همزمانی بالا شکست می‌خورد. یک رویکرد ساده ممکن است شامل تنظیم یک کلید با انقضا باشد، اما این منجر به «مشکل مرزی» می‌شود. یک کاربر می‌تواند ۱۰ درخواست در ثانیه ۰ و ۱۰ درخواست در ثانیه ۱ ارسال کند و عملاً سرعت انتقال خود را تنها با زمان‌بندی درخواست‌هایش در لبه پنجره‌های زمانی دو برابر کند. برای حل دقیق این مشکل، ما نیاز داریم که شمارش و تنظیم انقضا را به صورت اتمی بررسی کنیم.

چرا اسکریپت‌های Lua؟

Redis اسکریپت‌های Lua را به صورت اتمی اجرا می‌کند. این بدان معناست که یک بار که یک اسکریپت شروع به اجرا کند، توسط هیچ دستور دیگری قطع نخواهد شد. این ویژگی برای محدودیت نرخ ضروری است زیرا ما نیاز داریم:

  1. بررسی کنیم که آیا کلید محدودیت نرخ وجود دارد یا خیر.
  2. شمارش فعلی آن را بازیابی کنیم.
  3. شمارش را افزایش دهیم.
  4. در صورتی که این اولین درخواست در پنجره است، انقضا را تنظیم کنیم.
  5. وضعیت را به فراخواننده بازگردانیم.

انجام این کار با دستورات جداگانه GET و SET مستعد شرایط مسابقه (Race Conditions) است. با تعبیه این منطق در یک اسکریپت Lua، ما تضمین می‌کنیم که کل بلوک منطق به عنوان یک واحد واحد و جدایی‌ناپذیر اجرا می‌شود.

پیاده‌سازی شمارنده پنجره لغزنده

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

-- کلیدی برای ذخیره شمارنده محدودیت نرخ
local key = KEYS[1]
-- زمان فعلی بر حسب میلی‌ثانیه
local now = tonumber(ARGV[1])
-- اندازه پنجره بر حسب میلی‌ثانیه
local window = tonumber(ARGV[2])
-- حداکثر درخواست‌های مجاز
local limit = tonumber(ARGV[3])

-- محاسبه شروع پنجره فعلی
local window_start = now - window

-- حذف موارد منقضی شده (پاکسازی ساده، اگرچه برای پنجره‌های لغزنده دقیق، یک مجموعه مرتب بهتر است)
-- برای پنجره ثابت، ما فقط بررسی می‌کنیم که کلید وجود دارد
local count = redis.call('GET', key)

if not count then
    -- اولین درخواست در این پنجره
    redis.call('SET', key, 1)
    redis.call('EXPIRE', key, math.ceil(window / 1000))
    return {1, 1}
else
    count = tonumber(count)
    if count < limit then
        -- درخواست مجاز است
        redis.call('INCR', key)
        return {0, count + 1}
    else
        -- محدودیت نرخ تجاوز شده است
        return {1, count}
    end
end

در این اسکریپت، KEYS[1] نماینده شناسه منحصر به فرد برای مشتری است (مثلاً آدرس IP یا کلید API آن‌ها). ARGV زمان‌بندی فعلی، مدت زمان پنجره و حداکثر محدودیت مجاز را ارسال می‌کند. اسکریپت یک آرایه را بازگردانده که عنصر اول نشان می‌دهد آیا درخواست مسدود شده است (1) یا مجاز بوده است (0)، و عنصر دوم شمارش درخواست فعلی را ارائه می‌دهد.

یکپارچه‌سازی عملی با Python

یکپارچه‌سازی این با یک برنامه Python Flask با استفاده از کتابخانه redis-py ساده است. شما اسکریپت را یک بار در کلاینت Redis بارگذاری می‌کنید و سپس آن را از طریق مرجع فراخوانی می‌کنید.

import time
import redis

# راه‌اندازی کلاینت Redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# بارگذاری اسکریپت Lua
lua_script = """
-- (اسکریپت Lua از بالا را اینجا وارد کنید)
"""
rate_limit_fn = r.register_script(lua_script)

def check_rate_limit(user_id, limit=10, window=60000):
    now = int(time.time() * 1000)
    key = f"rate_limit:{user_id}"
    
    # اجرای اسکریپت Lua
    result = rate_limit_fn(keys=[key], args=[now, window, limit])
    
    status, count = result
    
    if status == 1:
        # بازگرداندن 429 Too Many Requests
        return False, count
    
    # بازگرداندن 200 OK
    return True, count

نتیجه‌گیری

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

Share: