İçeriğe geç
Node.js

Node.js Rate Limiting: API Koruma Stratejileri

Tarık Tunç
Node.js Rate Limiting: API Koruma Stratejileri

Node.js Rate Limiting: API Koruma Stratejileri

Rate limiting, API'nizi aşırı kullanımdan, DDoS saldırılarından ve kötü amaçlı botlardan korumanın en temel stratejilerinden biridir. Belirli bir zaman penceresi içinde yapılabilecek istek sayısını sınırlayarak sunucu kaynaklarınızı korur ve tüm kullanıcılar için adil erişim sağlar. Bu rehberde Node.js uygulamalarında rate limiting stratejilerini ve implementasyonlarını detaylı olarak inceleyeceğiz.

1. Rate Limiting Neden Gerekli?

  • DDoS koruması: Sunucunun aşırı isteklerle çökertilmesini engeller
  • Brute-force engelleme: Login denemelerini sınırlar
  • API maliyeti kontrolü: Üçüncü parti API çağrılarını yönetir
  • Adil kullanım: Tek bir kullanıcının tüm kaynakları tüketmesini önler
  • Crawling/scraping engelleme: Bot trafiğini kontrol eder

2. Rate Limiting Algoritmaları

Fixed Window

Sabit zaman pencereleri içinde istek sayısını sayar. Basit ama pencere sınırlarında burst'lere açıktır.

Sliding Window

Pencereyi sürekli kaydırır, daha adil bir dağılım sağlar.

Token Bucket

Belirli hızda token eklenir, her istek bir token harcar. Burst'lere izin verirken ortalama hızı kontrol eder.

Leaky Bucket

İstekler sabit hızda işlenir, fazlası kuyrukta bekler veya reddedilir.

3. express-rate-limit ile Temel Uygulama

# Kurulum
npm install express-rate-limit
const express = require('express');
const rateLimit = require('express-rate-limit');

const app = express();

// Genel rate limiter
const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 dakika
  max: 100,                   // pencere başına 100 istek
  standardHeaders: true,      // RateLimit-* headers
  legacyHeaders: false,       // X-RateLimit-* headers kapat
  message: {
    status: 429,
    error: 'Çok fazla istek gönderdiniz. Lütfen 15 dakika sonra tekrar deneyin.',
  },
  keyGenerator: (req) => {
    // IP bazlı (varsayılan) veya kullanıcı bazlı
    return req.user?.id || req.ip;
  },
  skip: (req) => {
    // Belirli IP'leri veya route'ları atla
    return req.ip === '127.0.0.1';
  },
});

app.use('/api/', generalLimiter);

Endpoint Bazlı Rate Limiting

// Auth endpoint'leri — sıkı limit
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Çok fazla giriş denemesi. 15 dakika bekleyin.' },
  skipSuccessfulRequests: true, // Başarılı istekleri sayma
});

// Arama endpoint'i — orta limit
const searchLimiter = rateLimit({
  windowMs: 60 * 1000,  // 1 dakika
  max: 30,
  message: { error: 'Çok fazla arama isteği.' },
});

// Dosya yükleme — düşük limit
const uploadLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 saat
  max: 10,
  message: { error: 'Dosya yükleme limiti aşıldı.' },
});

app.use('/api/auth/', authLimiter);
app.use('/api/search', searchLimiter);
app.use('/api/upload', uploadLimiter);

4. Redis ile Dağıtık Rate Limiting

Birden fazla sunucu çalıştırıyorsanız, in-memory rate limiting yeterli olmaz. Redis ile merkezi rate limiting uygulamanız gerekir.

# Kurulum
npm install rate-limit-redis ioredis
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const redisClient = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: 6379,
  enableOfflineQueue: false,
});

const redisLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  store: new RedisStore({
    sendCommand: (...args) => redisClient.call(...args),
  }),
});

5. Custom Rate Limiter — Token Bucket

// rateLimiter/tokenBucket.js
const Redis = require('ioredis');
const redis = new Redis();

class TokenBucketLimiter {
  constructor(options) {
    this.maxTokens = options.maxTokens || 10;
    this.refillRate = options.refillRate || 1; // saniyede eklenen token
    this.prefix = options.prefix || 'rl';
  }

  async consume(key, tokens = 1) {
    const now = Date.now();
    const bucketKey = `${this.prefix}:${key}`;

    // Lua script ile atomik işlem
    const script = `
      local key = KEYS[1]
      local maxTokens = tonumber(ARGV[1])
      local refillRate = tonumber(ARGV[2])
      local now = tonumber(ARGV[3])
      local requested = tonumber(ARGV[4])

      local bucket = redis.call('hmget', key, 'tokens', 'lastRefill')
      local tokens = tonumber(bucket[1]) or maxTokens
      local lastRefill = tonumber(bucket[2]) or now

      -- Token yenileme
      local elapsed = (now - lastRefill) / 1000
      tokens = math.min(maxTokens, tokens + (elapsed * refillRate))

      if tokens >= requested then
        tokens = tokens - requested
        redis.call('hmset', key, 'tokens', tokens, 'lastRefill', now)
        redis.call('expire', key, math.ceil(maxTokens / refillRate) + 1)
        return {1, math.ceil(tokens)}
      else
        redis.call('hmset', key, 'tokens', tokens, 'lastRefill', now)
        return {0, math.ceil(tokens)}
      end
    `;

    const [allowed, remaining] = await redis.eval(
      script, 1, bucketKey,
      this.maxTokens, this.refillRate, now, tokens
    );

    return { allowed: allowed === 1, remaining };
  }
}

// Middleware olarak kullanım
function tokenBucketMiddleware(options) {
  const limiter = new TokenBucketLimiter(options);

  return async (req, res, next) => {
    const key = req.user?.id || req.ip;
    const { allowed, remaining } = await limiter.consume(key);

    res.set('X-RateLimit-Remaining', remaining);

    if (!allowed) {
      return res.status(429).json({
        error: 'Rate limit aşıldı',
        remaining,
      });
    }

    next();
  };
}

app.use('/api/', tokenBucketMiddleware({
  maxTokens: 100,
  refillRate: 2, // saniyede 2 token
}));

6. Sliding Window Implementasyonu

// rateLimiter/slidingWindow.js
class SlidingWindowLimiter {
  constructor(redis, options) {
    this.redis = redis;
    this.windowMs = options.windowMs || 60000;
    this.max = options.max || 100;
    this.prefix = options.prefix || 'sw';
  }

  async isAllowed(key) {
    const now = Date.now();
    const windowStart = now - this.windowMs;
    const redisKey = `${this.prefix}:${key}`;

    const pipeline = this.redis.pipeline();
    pipeline.zremrangebyscore(redisKey, 0, windowStart); // Eski kayıtları sil
    pipeline.zadd(redisKey, now, `${now}-${Math.random()}`); // Yeni istek ekle
    pipeline.zcard(redisKey); // Toplam sayı
    pipeline.expire(redisKey, Math.ceil(this.windowMs / 1000));

    const results = await pipeline.exec();
    const count = results[2][1];

    if (count > this.max) {
      // Son eklenen kaydı sil (reddedilen istek)
      await this.redis.zremrangebyscore(redisKey, now, now);
      return { allowed: false, remaining: 0, count };
    }

    return { allowed: true, remaining: this.max - count, count };
  }
}
İpucu: Sliding window, fixed window'a göre daha adildir ancak Redis'te daha fazla bellek kullanır. Yüksek trafikli uygulamalarda token bucket genellikle en iyi dengedir.

7. API Tier ve Plan Bazlı Limiting

// Plan bazlı rate limiting
const plans = {
  free:    { windowMs: 60 * 60 * 1000, max: 100 },   // 100/saat
  basic:   { windowMs: 60 * 60 * 1000, max: 1000 },  // 1000/saat
  pro:     { windowMs: 60 * 60 * 1000, max: 10000 },  // 10000/saat
  enterprise: { windowMs: 60 * 60 * 1000, max: 100000 },
};

function planBasedLimiter(req, res, next) {
  const userPlan = req.user?.plan || 'free';
  const config = plans[userPlan];

  const limiter = rateLimit({
    ...config,
    keyGenerator: (req) => req.user?.apiKey || req.ip,
    standardHeaders: true,
    handler: (req, res) => {
      res.status(429).json({
        error: 'Rate limit aşıldı',
        plan: userPlan,
        limit: config.max,
        upgrade: 'Daha yüksek limit için planınızı yükseltin.',
      });
    },
  });

  limiter(req, res, next);
}

8. Rate Limit Headers

// Standart rate limit headers
// RateLimit-Limit: 100          — Pencere başına izin verilen toplam istek
// RateLimit-Remaining: 95       — Kalan istek sayısı
// RateLimit-Reset: 1679012400   — Pencere sıfırlanma zamanı (Unix timestamp)
// Retry-After: 900              — Tekrar denemek için beklenmesi gereken saniye

// Custom header middleware
function rateLimitHeaders(req, res, next) {
  const originalJson = res.json.bind(res);
  res.json = function(data) {
    if (res.statusCode === 429) {
      res.set('Retry-After', '900');
    }
    return originalJson(data);
  };
  next();
}
Uyarı: Reverse proxy (Nginx, CloudFlare) arkasında çalışıyorsanız app.set('trust proxy', 1) ayarını yapın. Aksi takdirde tüm istekler aynı IP'den geliyormuş gibi görünür.

Sonuç

Rate limiting, API güvenliğinin temel taşlarından biridir. Basit uygulamalar için express-rate-limit yeterli olurken, dağıtık sistemlerde Redis tabanlı çözümler gereklidir. Token bucket ve sliding window algoritmaları farklı senaryolara göre tercih edilebilir. Endpoint bazlı ve plan bazlı farklılaştırma ile hem güvenliği hem de kullanıcı deneyimini optimize edin. Rate limit header'ları ile istemcilere kalan hakları hakkında bilgi verin.

Kaynaklar

İlgili Yazılar