Rate Limiting ב-API: הגנה מפני ניצול לרעה

Rate limiting היא טכניקה חיונית להגנה על API מפני ניצול לרעה, צריכת משאבים מופרזת והתקפות מניעת שירות. מימוש נכון מבטיח זמינות למשתמשים לגיטימיים תוך חסימת התנהגות זדונית או מופרזת.

מדוע ליישם Rate Limiting?

  • הגנה מפני DDoS: הפחתת התקפות מניעת שירות
  • מניעת Scraping: הקשיה על חילוץ נתונים אוטומטי
  • בקרת עלויות: מניעת צריכה מופרזת של משאבי מחשוב
  • הבטחת איכות השירות: חלוקת משאבים באופן הוגן
  • מניעת Brute Force: הגבלת ניסיונות אימות

אלגוריתמים של Rate Limiting

1. Token Bucket

אלגוריתם המתחזק "דלי" של tokens שמתמלאים מחדש לאורך זמן:

      class TokenBucket {
      constructor(capacity, refillRate) {
      this.capacity = capacity;        // Capacidade máxima do balde
      this.tokens = capacity;          // Tokens disponíveis
      this.refillRate = refillRate;    // Tokens por segundo
      this.lastRefill = Date.now();
      }
      tryConsume(tokens = 1) {
      this.refill();
      if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true;  // Requisição permitida
      }
      return false;   // Rate limit excedido
      }
      refill() {
      const now = Date.now();
      const timePassed = (now - this.lastRefill) / 1000;
      const tokensToAdd = timePassed * this.refillRate;
      this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
      this.lastRefill = now;
      }
      }
      // Uso: 100 requisições máximo, recarrega 10/segundo
      const bucket = new TokenBucket(100, 10);
      

יתרונות: מאפשר bursts מבוקרים, מחליק את התעבורה

חסרונות: מורכב יותר למימוש

2. Leaky Bucket

מעבד בקשות בקצב קבוע, כמו מים הדולפים מדלי:

  • הבקשות נכנסות לדלי
  • מעובדות בקצב קבוע
  • העודף עולה על גדותיו (נדחה)
  • מבטיח פלט אחיד

3. Fixed Window

סופר בקשות בחלונות זמן קבועים:

      class FixedWindowRateLimiter {
      constructor(maxRequests, windowMs) {
      this.maxRequests = maxRequests;
      this.windowMs = windowMs;
      this.requests = new Map();
      }
      isAllowed(userId) {
      const now = Date.now();
      const windowStart = Math.floor(now / this.windowMs) * this.windowMs;
      const key = \`\$:\$\`;
      const count = this.requests.get(key) || 0;
      if (count < this.maxRequests) {
      this.requests.set(key, count + 1);
      return true;
      }
      return false;
      }
      }
      // 100 requisições por hora
      const limiter = new FixedWindowRateLimiter(100, 60 * 60 * 1000);
      

בעיה: מאפשר עד פי 2 מהמגבלה בקצוות החלונות

4. Sliding Window Log

מתחזק יומן של חותמות זמן של בקשות:

  • מאחסן חותמת זמן לכל בקשה
  • מסיר בקשות מחוץ לחלון
  • מדויק יותר מ-Fixed Window
  • צריכת זיכרון גבוהה יותר

5. Sliding Window Counter

משלב Fixed Window עם החלקה:

      // Calcula uma média ponderada entre janelas atual e anterior
      const currentWindowCount = getCurrentWindowCount(userId);
      const previousWindowCount = getPreviousWindowCount(userId);
      const percentageInCurrentWindow = (now - currentWindowStart) / windowSize;
      const estimatedCount =
      previousWindowCount * (1 - percentageInCurrentWindow) +
      currentWindowCount;
      return estimatedCount < maxRequests;
      

מימוש מעשי

עם Redis (מומלץ לסביבת ייצור)

      import Redis from 'ioredis';
      const redis = new Redis();
      async function checkRateLimit(userId, maxRequests = 100, windowSeconds = 60) {
      const key = \`rate_limit:\$\`;
      const now = Date.now();
      const windowStart = now - (windowSeconds * 1000);
      // Remover requisições antigas
      await redis.zremrangebyscore(key, 0, windowStart);
      // Contar requisições na janela
      const requestCount = await redis.zcard(key);
      if (requestCount < maxRequests) {
      // Adicionar nova requisição
      await redis.zadd(key, now, \`\$-\${Math.random()}\`);
      await redis.expire(key, windowSeconds);
      return { allowed: true, remaining: maxRequests - requestCount - 1 };
      }
      return { allowed: false, remaining: 0 };
      }
      // Middleware Express
      app.use(async (req, res, next) => {
      const userId = req.user?.id || req.ip;
      const result = await checkRateLimit(userId);
      res.set({
      'X-RateLimit-Limit': 100,
      'X-RateLimit-Remaining': result.remaining,
      'X-RateLimit-Reset': new Date(Date.now() + 60000).toISOString()
      });
      if (!result.allowed) {
      return res.status(429).json({
      error: 'Too Many Requests',
      retryAfter: 60
      });
      }
      next();
      });
      

ספריות פופולריות

  • express-rate-limit: Middleware עבור Express.js
  • rate-limiter-flexible: תומך במספר backends (Redis, Memcached, MySQL)
  • Kong Rate Limiting: Plugin עבור API Gateway
  • AWS API Gateway: Rate limiting מובנה

אסטרטגיות מתקדמות

Rate Limiting היררכי

  • גלובלי: מגבלה כוללת של ה-API (לדוגמה: 1M req/min)
  • לכל משתמש: מגבלה אישית (לדוגמה: 1000 req/min)
  • לכל Endpoint: מגבלות ספציפיות (login: 5 req/min)
  • לכל IP: הגנה נוספת מפני ניצול לרעה

Rate Limiting דינמי

  • התאמת מגבלות בהתבסס על עומס המערכת
  • הגדלת מגבלות עבור משתמשי פרימיום
  • הפחתת מגבלות במהלך תקריות

Whitelisting ו-Blacklisting

  • פטור ל-IP/משתמשים מהימנים
  • חסימה קבועה של תוקפים מוכרים
  • מימוש מערכת מוניטין

שיטות עבודה מומלצות

  • החזרת כותרות אינפורמטיביות (X-RateLimit-*)
  • שימוש בסטטוס HTTP 429 (Too Many Requests)
  • הכללת כותרת Retry-After
  • תיעוד ברור של המגבלות ב-API
  • מימוש backoff אקספוננציאלי בצד הלקוח
  • ניטור מדדי rate limiting
  • התראה על דפוסים חריגים
  • בדיקת מגבלות לפני סביבת ייצור

כלי ניטור

  • Grafana + Prometheus: הדמיית מדדי rate limiting
  • Datadog: ניטור והתראות
  • CloudWatch: עבור API ב-AWS
  • New Relic: APM עם תמיכה ב-rate limiting