Application Security

Sıfırdan Oran Sınırlama Uygulamak: Web Uygulamaları için Çekirdek Algoritmalar ve Durum Yönetimi

Oran sınırlama, basit bir özellikten öte, web uygulaması güvenliğinin ilk savunma hattıdır. Altyapınızı Hizmet Reddi (DoS) saldırılarından korur, API istismarını önler ve kullanıcılar arasında adil kaynak dağılımı sağlar. Geliştiricilerin çoğu Cloudflare veya AWS WAF gibi üçüncü taraf hizmetlere güvense de, sıfırdan oran sınırlama uygulama biçimini anlamak, özel çözümler geliştirmek veya hafif mikro hizmetleri optimize etmek için hayati önem taşır.

Bu yazıda, oran sınırlamanın arkasındaki çekirdek algoritmaları keşfedecek, durum yönetimi stratejilerini tartışacak ve JavaScript ile Go dillerinde pratik kod örnekleri sunacağız.

Sabit Pencere Algoritması: Sadelik vs. Tırmanma Etkisi

Sabit Pencere algoritması en basit yaklaşımdır. Zamanı sabit aralıklara (örneğin 1 dakikalık pencerelere) böler ve bu pencere içindeki istekleri sayar. Sayaç limiti aştığında, pencere sıfırlanana kadar sonraki istekler reddedilir.

Bu yöntem verimli olsa da "tırmanma etkisi"nden (cliff effect) muzdariptir. Bir kullanıcı, bir pencerenin sonuna çok yakın bir anda 100 istek yaparsa ve bir sonraki pencerenin başında 100 istek daha yaparsa, 2 dakikalık bir sürede 200 istek yaparak dakikada 200 isteklik limiti etkili bir şekilde aşmış olur. Bunu azaltmak için bazı sistemler örtüşen pencereler uygular ancak bu durum karmaşıklığı artırır.

JavaScript (Node.js) ile Uygulama

Aşağıda, bellek içi bir depolama kullanan Sabit Pencere yaklaşımıyla basit bir ara katman (middleware) örneği bulunmaktadır. Üretim ortamı için, dağıtılmış durumu yönetmek üzere bellek depolamasını Redis ile değiştirmeniz gerekir.

const RATE_LIMIT = 100;
const WINDOW_MS = 60000; // 1 dakika

const requests = new Map();

function fixedWindowLimit(req, res, next) {
  const ip = req.ip;
  const now = Date.now();
  const windowStart = Math.floor(now / WINDOW_MS) * WINDOW_MS;
  
  if (!requests.has(ip) || requests.get(ip).windowStart !== windowStart) {
    // Yeni pencere, sayacı sıfırla
    requests.set(ip, { count: 1, windowStart });
  } else {
    const current = requests.get(ip);
    if (current.count >= RATE_LIMIT) {
      return res.status(429).json({ error: "Too Many Requests" });
    }
    current.count++;
  }
  
  next();
}

Kayan Günlük Penceresi: Bir Bedele Sahip Hassasiyet

Tırmanma etkisini ortadan kaldırmak için Kayan Günlük Penceresi algoritması, her istekin zaman damgasını kaydeder. Yeni bir istek geldiğinde, sistem son N saniye içindeki istek sayısını hesaplar. Sayı limiti aşarsa istek reddedilir.

Bu yaklaşım yüksek hassasiyet sunar ancak önemli bir depolama maliyeti getirir. İstek sayısı arttıkça, periyodik temizlik uygulanmadıkça veya eski girdileri verimli bir şekilde atmak için sıralı bir veri yapısı kullanılmadıkça günlük sınırsız büyür.

Token Havuzu: Düzgün Trafik Akışı

Token Havuzu algoritması, adillik ve performans arasındaki dengeyi kurmak için genellikle altın standart olarak kabul edilir. Sabit bir kapasiteye sahip bir havuz hayal edin. Tokenlar, sabit bir hızla havuza eklenir. Her istek bir token tüketir. Havuz boşaldığında istek reddedilir.

Bu, tokenlar biriktirildiğinde trafik patlamalarına izin verirken uzun vadeli ortalama hızı sıkı bir şekilde sınırlar. Matematiksel olarak zariftir ve dağıtılmış sistemlerde iyi performans gösterir.

Go ile Uygulama

Go'nun eşzamanlılık modeli, token havuzları uygulamak için mükemmel bir seçimdir. İşte mutex ile korunmuş bir yapı kullanan sağlam bir uygulama.

package main

import (
	"sync"
	"time"
)

type TokenBucket struct {
	tokens     float64
	maxTokens  float64
	refillRate float64
	lastRefill time.Time
	mu         sync.Mutex
}

func NewTokenBucket(maxTokens, refillRate float64) *TokenBucket {
	return &TokenBucket{
		tokens:     maxTokens,
		maxTokens:  maxTokens,
		refillRate: refillRate,
		lastRefill: time.Now(),
	}
}

func (tb *TokenBucket) Allow() bool {
	tb.mu.Lock()
	defer tb.mu.Unlock()

	now := time.Now()
	elapsed := now.Sub(tb.lastRefill).Seconds()
	tb.tokens += elapsed * tb.refillRate

	if tb.tokens > tb.maxTokens {
		tb.tokens = tb.maxTokens
	}
	tb.lastRefill = now

	if tb.tokens >= 1.0 {
		tb.tokens--
		return true
	}
	return false
}

Durum Yönetimi: Arka Uç Seçiminiz

Üretim kalitesinde oran sınırlamanın en kritik yönü durum yönetimidir. JavaScript örneğinde gösterildiği gibi bellek içi haritalar yalnızca tek örneklili dağıtımlar için çalışır. Çok düğümlü ortamlarda paylaşılan bir duruma ihtiyacınız vardır.

  • Redis: Endüstri standardıdır. Kayan pencereler için sıralı kümeleri, sabit pencereler için basit hash'leri kullanın. Hızı ve atomik işlemleri, yüksek veri akışlı API'ler için ideal kılar.
  • Memcached: Basit anahtar-değer aramaları için Redis'ten daha hızlıdır ancak kayan pencereler için gereken veri yapılarına sahip değildir.
  • Dağıtık Kilitler: Bir veritabanı kullanmak zorundaysanız, yüksek eşzamanlılık sırasında yarış durumlarını önlemek için iyimser kilitleme veya veritabanı düzeyinde kısıtlamalar kullandığınızdan emin olun.

Sonuç

Sıfırdan oran sınırlama uygulamak, uygulamanızın güvenlik durumunu granüler bir şekilde kontrol etmenizi sağlar. Sabit Pencere algoritması anlaşılması kolay olsa da, Token Havuzu gerçek dünya trafik desenleri için daha iyi esneklik sunar. Hangi algoritmayı seçerseniz seçin, tutarlılığı dağıtılmış sistemler boyunca sağlamak için Redis gibi araçlar kullanarak verimli durum yönetimini her zaman önceliklendirin. Bu çekirdek algoritmaları ustalaşarak, uygulamanızı istismarlardan koruyan ve meşru kullanıcılar için pürüzsüz bir deneyim sağlayan dayanıklı bir temel oluşturursunuz.

Share: