Application Security

تطبيق تقييد المعدلات من الصفر: الخوارزميات الأساسية وإدارة الحالة لتطبيقات الويب

تقييد المعدلات ليس مجرد ميزة؛ بل هو خط الدفاع الأول في أمان تطبيقات الويب. فهو يحمي بنيتك التحتية من هجمات حجب الخدمة (DoS)، ويمنع إساءة استخدام واجهات برمجة التطبيقات (API)، ويضمن تخصيص الموارد بعدالة بين المستخدمين. بينما يعتمد العديد من المطورين على خدمات الطرف الثالث مثل Cloudflare أو AWS WAF، فإن فهم كيفية تطبيق تقييد المعدلات من الصفر أمر بالغ الأهمية لبناء حلول مخصصة أو تحسين الخدمات المصغرة خفيفة الوزن.

في هذا المنشور، سنستكشف الخوارزميات الأساسية وراء تقييد المعدلات، ونناقش استراتيجيات إدارة الحالة، ونقدم أمثلة برمجية عملية بلغتي JavaScript و Go.

خوارزمية النافذة الثابتة: البساطة مقابل تأثير الانهيار

تعتبر خوارزمية النافذة الثابتة (Fixed Window) أبسط نهج. فهي تقسم الوقت إلى فترات ثابتة (مثل نوافذ مدتها دقيقة واحدة) وتعد الطلبات داخل تلك النافذة. بمجرد تجاوز العداد للحد المسموح به، يتم رفض الطلبات اللاحقة حتى يتم إعادة تعيين النافذة.

على الرغم من كفاءتها، تعاني هذه الطريقة من "تأثير الانهيار" (Cliff Effect). إذا قام مستخدم بإرسال 100 طلب في نهاية نافذة زمنية واحدة و100 طلب في بداية النافذة التالية، فقد تجاوز فعلياً حد 200 طلب في الدقيقة من خلال إرسال 200 طلب في غضون دقيقتين. للتخفيف من هذا الأمر، تطبق بعض الأنظمة نوافذ متداخلة، لكن ذلك يضيف تعقيداً.

التنفيذ بلغة JavaScript (Node.js)

يُظهر المثال أدناه وسيطاً بسيطاً (Middleware) يستخدم نهج النافذة الثابتة مع مخزن في الذاكرة. لاحظ أنه للإنتاج، ستستبدل مخزن الذاكرة بـ Redis للتعامل مع الحالة الموزعة.

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

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) {
    // New window, reset counter
    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();
}

سجل النافذة المنزلقة: الدقة مقابل التكلفة

للتخلص من تأثير الانهيار، تسجل خوارزمية سجل النافذة المنزلقة (Sliding Window Log) الطابع الزمني لكل طلب. عند وصول طلب جديد، يحسب النظام عدد الطلبات في آخر N ثانية. إذا تجاوز العدد الحد المسموح به، يتم رفض الطلب.

يوفر هذا النهج دقة عالية، لكنه يأتي بتكلفة تخزين كبيرة. ومع زيادة عدد الطلبات، ينمو السجل إلى ما لا نهاية إلا إذا نفذت تنظيفاً دورياً أو استخدمت بنية بيانات مرتبة للتخلص بكفاءة من السجلات القديمة.

دلو الرموز: تدفق حركة مرور سلس

تُعتبر خوارزمية دلو الرموز (Token Bucket) غالباً المعيار الذهبي لتحقيق التوازن بين الإنصاف والأداء. تخيل دلوًا بسعة ثابتة. يتم إضافة الرموز إلى الدلو بمعدل ثابت. يستهلك كل طلب رمزاً واحداً. إذا كان الدلو فارغاً، يتم رفض الطلب.

يسمح هذا بحدوث انفجارات في حركة المرور (إذا تم تراكم الرموز) مع تحديد الحد الأقصى بدقة للمعدل طويل الأمد. وهي أنيقة رياضياً وتعمل بشكل جيد في الأنظمة الموزعة.

التنفيذ بلغة Go

نموذج التزامن في لغة Go يجعلها خياراً ممتازاً لتنفيذ دلاء الرموز. إليك تنفيذ قوي باستخدام هيكل محمي بـ Mutex.

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
}

إدارة الحالة: اختيار قاعدة البيانات الخلفية

الجانب الأكثر أهمية في تقييد المعدلات الجاهز للإنتاج هو إدارة الحالة. تعمل خرائط الذاكرة، كما هو موضح في مثال JavaScript، فقط في عمليات النشر ذات المثيل الواحد. في البيئات متعددة العقد، تحتاج إلى حالة مشتركة.

  • Redis: المعيار الصناعي. استخدم المجموعات المرتبة (Sorted Sets) للنافذة المنزلقة أو الهاشات البسيطة للنافذة الثابتة. تجعل سرعته وعملياته الذرية منه مثالياً لواجهات برمجة التطبيقات عالية الإنتاجية.
  • Memcached: أسرع من Redis لإجراءات البحث البسيطة عن المفاتيح والقيم، لكنه يفتقر إلى هياكل البيانات اللازمة للنافذة المنزلقة.
  • القفل الموزع: إذا كان عليك استخدام قاعدة بيانات، فتأكد من استخدام القفل التفاؤلي أو قيود مستوى قاعدة البيانات لمنع ظروف السباق أثناء التزامن العالي.

الخاتمة

يمنحك تطبيق تقييد المعدلات من الصفر تحكماً دقيقاً في موقف أمان تطبيقك. بينما تكون خوارزمية النافذة الثابتة سهلة الفهم، فإن دلو الرموز يوفر مرونة أفضل لأنماط حركة المرور في العالم الحقيقي. بغض النظر عن الخوارزمية التي تختارها، أعطِ دائماً الأولوية لإدارة الحالة بكفاءة باستخدام أدوات مثل Redis لضمان الاتساق عبر الأنظمة الموزعة. من خلال إتقان هذه الخوارزميات الأساسية، تبني أساساً مرناً يحمي تطبيقك من الإساءة مع الحفاظ على تجربة سلسة للمستخدمين الشرعيين.

Share: