API における Rate Limiting:不正利用に対する保護

Rate limiting は、API を不正利用、過剰なリソース消費、サービス拒否攻撃から保護するための重要な技術です。適切な実装により、悪意のある行動や過剰な行動を遮断しながら、正当なユーザーへの可用性を確保します。

なぜ Rate Limiting を実装するのか?

  • DDoS に対する保護:サービス拒否攻撃を緩和する
  • Scraping の防止:自動化されたデータ抽出を困難にする
  • コストの管理:計算リソースの過剰な消費を回避する
  • サービス品質の確保:リソースを公平に分配する
  • Brute Force の防止:認証試行を制限する

Rate Limiting アルゴリズム

1. Token Bucket

時間の経過とともに補充されるトークンの「バケツ」を維持するアルゴリズム:

      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);
      

利点:制御されたバーストを許可し、トラフィックを平滑化する

欠点:実装がより複雑である

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:Express.js 用の Middleware
  • rate-limiter-flexible:複数の backends に対応(Redis、Memcached、MySQL)
  • Kong Rate Limiting:API Gateway 用の Plugin
  • 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 で制限を明確に文書化する
  • クライアントで指数バックオフを実装する
  • rate limiting のメトリクスを監視する
  • 異常なパターンについてアラートを出す
  • 本番環境の前に制限をテストする

監視ツール

  • Grafana + Prometheus:rate limiting メトリクスを可視化する
  • Datadog:監視とアラート
  • CloudWatch:AWS 上の API 向け
  • New Relic:rate limiting に対応した APM