API غير الآمنة هي بوابة مفتوحة للهاكرز. في هذا الدليل العملي، نكشف كيف تبني نظام مصادقة متين، تحمي البيانات في النقل والراحة، وتطبق Rate Limiting ذكي دون أن تخنق الأداء. كل ذلك بأكواد حقيقية وخبرات من شركات مثل Stripe وTwilio.
في عام 2023، تعرضت شركة Cloudflare لهجوم DDoS وصل إلى 71 مليون طلب في الثانية. السبب؟ API مفتوح بدون Rate Limiting. وفي نفس العام، سربت شركة Okta بيانات 5000 عميل بسبب ثغرة في نظام Authentication. الحقيقة القاسية هي أن معظم المطورين يعاملون أمن الـ API كخطوة أخيرة، وليس كطبقة أساسية في التصميم. المشكلة الأكبر؟ الكثير من الأدوات الأمنية تبطئ الأداء لدرجة تجعل المستخدمين يهربون. فكيف تبني API آمن وسريع في نفس الوقت؟
الأمان ليس مجرد إضافة طبقة تشفير هنا أو هناك. إنه نظام متكامل يبدأ من اللحظة التي يضغط فيها المستخدم على زر "Login" وينتهي عند الحد الأقصى للطلبات التي يسمح بها السيرفر في الدقيقة. في هذا المقال، سنفكك كل طبقة أمنية ونشرح كيف تعمل خلف الكواليس، من الـ Memory Allocation في الـ JWT Tokens إلى الـ Event Loop في الـ Rate Limiting. لن نتحدث عن نظريات أكاديمية، بل عن الكود الحقيقي الذي يعمل في الإنتاج اليوم.
الكثير من المطورين يعتقدون أن استخدام JWT يكفي لجعل الـ API آمناً. الحقيقة؟ JWT هو مجرد حاوية للبيانات، وليس حلاً سحرياً. المشكلة الأساسية في JWT هي أنه يعتمد على التوقيع الرقمي فقط، وليس على التشفير. هذا يعني أن أي شخص يستطيع فك تشفير الـ Base64 للـ Payload ويرى كل البيانات بداخلها، حتى لو لم يستطع تعديلها. في عام 2021، اكتشفت ثغرة في نظام مصادقة أحد البنوك الكبرى حيث كان الـ JWT يحتوي على معلومات حساسة مثل الـ User ID وPermissions بدون تشفير. النتيجة؟ المهاجمون استطاعوا قراءة هذه البيانات بسهولة واستخدامها في هجمات أخرى.
الحل؟ لا تضع أبداً بيانات حساسة في الـ JWT Payload. استخدم الـ JWT فقط لحمل الـ User ID ومعلومات غير حساسة مثل الـ Expiration Time. وإذا كنت مضطراً لوضع بيانات حساسة، استخدم الـ JWE (JSON Web Encryption) بدلاً من الـ JWT العادي. أيضاً، لا تعتمد على الـ Client Side لتخزين الـ JWT. استخدم الـ HttpOnly وSecure Cookies لمنع هجمات XSS وCSRF. إليك مثال على كيفية توليد JWT آمن في Node.js:
// Never store sensitive data in JWT payload
const jwt = require('jsonwebtoken');
const secretKey = process.env.JWT_SECRET; // 32+ characters, stored in env
function generateToken(userId) {
// Only include non-sensitive data
const payload = {
sub: userId, // Subject (user ID)
iat: Math.floor(Date.now() / 1000), // Issued at
exp: Math.floor(Date.now() / 1000) + (60 * 60) // Expires in 1 hour
};
// Sign with HS256 (HMAC-SHA256)
return jwt.sign(payload, secretKey, { algorithm: 'HS256' });
}
// Set as HttpOnly, Secure, SameSite cookie
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 3600000 // 1 hour
});لاحظ أننا استخدمنا خوارزمية HS256 للتوقيع، وهي تعتمد على مفتاح سري. هذا المفتاح يجب أن يكون طويلاً ومعقداً (32 حرفاً على الأقل) ومخزن في متغيرات البيئة. أيضاً، استخدمنا HttpOnly Cookie لمنع الوصول إلى الـ Token من خلال JavaScript، مما يحمي من هجمات XSS. لكن حتى مع هذه الإجراءات، يبقى الـ JWT معرضاً لهجمات مثل الـ Token Replay. الحل؟ استخدم الـ JWT مع نظام قصير العمر مثل الـ Refresh Tokens.
الـ Refresh Tokens هي طريقة ذكية لتجنب إعادة تسجيل الدخول كل ساعة دون التضحية بالأمان. الفكرة بسيطة: بدلاً من إصدار JWT طويل الأمد، تصدر JWT قصير الأمد (ساعة مثلاً) وRefresh Token طويل الأمد (أسبوع أو شهر). عندما تنتهي صلاحية الـ JWT، يرسل العميل الـ Refresh Token إلى السيرفر للحصول على JWT جديد. لكن كيف تضمن أن الـ Refresh Token نفسه آمن؟
أولاً، يجب أن يكون الـ Refresh Token عشوائياً وغير متوقع. لا تستخدم الـ User ID أو أي بيانات قابلة للتخمين. ثانياً، يجب تخزين الـ Refresh Token في قاعدة بيانات مع معلومات إضافية مثل الـ IP Address وDevice Type. عندما يأتي طلب للحصول على JWT جديد، تحقق من أن الـ IP Address لم يتغير. إذا تغير، فهذا قد يكون علامة على سرقة الـ Token. إليك مثال على كيفية تنفيذ هذا النظام:
import secrets
import jwt
from datetime import datetime, timedelta
# Store in database (example schema: refresh_tokens)
# id | user_id | token | ip_address | device_type | created_at | expires_at
def generate_refresh_token(user_id, ip_address, device_type):
token = secrets.token_urlsafe(32) # 32 bytes = 256 bits
expires_at = datetime.utcnow() + timedelta(days=30)
# Store in database (pseudo-code)
db.execute(
"INSERT INTO refresh_tokens VALUES (?, ?, ?, ?, ?, ?)",
(None, user_id, token, ip_address, device_type, datetime.utcnow(), expires_at)
)
return token
def refresh_jwt(refresh_token, current_ip):
# Check if token exists and is not expired
token_data = db.execute(
"SELECT user_id, ip_address FROM refresh_tokens WHERE token = ? AND expires_at > ?",
(refresh_token, datetime.utcnow())
).fetchone()
if not token_data:
raise Exception("Invalid or expired refresh token")
# Check if IP changed (possible token theft)
if token_data['ip_address'] != current_ip:
# Log suspicious activity
db.execute(
"INSERT INTO security_logs VALUES (?, ?, ?, ?)",
(None, token_data['user_id'], "IP_CHANGE", datetime.utcnow())
)
raise Exception("IP address changed. Please login again.")
# Generate new JWT
payload = {
"sub": token_data['user_id'],
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(hours=1)
}
return jwt.encode(payload, os.getenv('JWT_SECRET'), algorithm='HS256')الكثير من المطورين يعتقدون أن استخدام HTTPS يكفي لحماية البيانات. الحقيقة؟ HTTPS يحمي البيانات أثناء النقل فقط، وليس أثناء الراحة (At Rest). إذا كان الـ API يخزن بيانات حساسة في قاعدة البيانات بدون تشفير، فإن اختراق قاعدة البيانات يعني كشف كل البيانات. في عام 2022، تعرضت شركة LastPass لاختراق حيث سرق المهاجمون قاعدة بيانات كاملة تحتوي على معلومات حساسة. السبب؟ البيانات كانت مشفرة باستخدام مفتاح واحد مخزن في نفس السيرفر. النتيجة؟ المهاجمون استطاعوا فك تشفير كل البيانات بسهولة.
الحل؟ استخدم تشفير من طرف إلى طرف (End-to-End Encryption). هذا يعني أن البيانات تُشفّر على جهاز العميل قبل إرسالها إلى السيرفر، وتبقى مشفرة حتى تصل إلى المستلم المقصود. لكن كيف تنفذ هذا دون التأثير على الأداء؟ استخدم خوارزميات تشفير سريعة مثل AES-256-GCM. هذه الخوارزمية توفر تشفيراً سريعاً وآمناً، وتدعم أيضاً التحقق من سلامة البيانات (Authentication Tag). إليك مثال على كيفية تشفير البيانات في الـ Client Side قبل إرسالها إلى الـ API:
// Client-side encryption using Web Crypto API
async function encryptData(data, publicKey) {
// Convert data to Uint8Array
const dataBuffer = new TextEncoder().encode(JSON.stringify(data));
// Generate a random AES key
const aesKey = await window.crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // extractable
["encrypt", "decrypt"]
);
// Generate a random IV (Initialization Vector)
const iv = window.crypto.getRandomValues(new Uint8Array(12));
// Encrypt data with AES-GCM
const encryptedData = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
aesKey,
dataBuffer
);
// Export the AES key and encrypt it with RSA public key
const exportedAesKey = await window.crypto.subtle.exportKey("raw", aesKey);
const encryptedAesKey = await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
publicKey,
exportedAesKey
);
// Return IV, encrypted data, and encrypted AES key
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(encryptedData)),
key: Array.from(new Uint8Array(encryptedAesKey))
};
}في هذا المثال، استخدمنا الـ Web Crypto API لتشفير البيانات باستخدام AES-256-GCM، ثم قمنا بتشفير مفتاح AES باستخدام RSA-OAEP. هذا يضمن أن حتى إذا اخترق المهاجم السيرفر، لن يستطيع فك تشفير البيانات بدون المفتاح الخاص الذي يملكه العميل فقط. لكن ماذا عن الأداء؟ AES-256-GCM سريع جداً ويمكنه تشفير ميغابايتات من البيانات في أجزاء من الثانية. المشكلة الحقيقية تأتي من الـ Key Management. إذا فقد العميل مفتاحه الخاص، سيفقد الوصول إلى بياناته للأبد. الحل؟ استخدم نظام نسخ احتياطي للمفاتيح مع تشفير إضافي.
إذا كنت تخزن بيانات حساسة في قاعدة البيانات، يجب أن تكون مشفرة حتى أثناء الراحة. لكن تشفير كل حقل يدوياً في الكود سيؤثر على الأداء ويجعل الاستعلامات صعبة. الحل؟ استخدم تشفيراً شفافاً (Transparent Data Encryption) مثل الذي توفره PostgreSQL مع pgcrypto أو MongoDB مع Encrypted Storage Engine. هذه الأدوات تشفر البيانات تلقائياً عند إدخالها وتفك تشفيرها عند استرجاعها، دون الحاجة لتعديل الكود. إليك مثال على كيفية استخدام pgcrypto في PostgreSQL:
-- Enable pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Create table with encrypted columns
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email BYTEA, -- Encrypted
password BYTEA, -- Encrypted (hashed password)
ssn BYTEA, -- Encrypted (Social Security Number)
created_at TIMESTAMP DEFAULT NOW()
);
-- Insert encrypted data
INSERT INTO users (email, password, ssn) VALUES (
pgp_sym_encrypt('user@example.com', 'db_encryption_key'),
pgp_sym_encrypt('hashed_password_here', 'db_encryption_key'),
pgp_sym_encrypt('123-45-6789', 'db_encryption_key')
);
-- Query encrypted data
SELECT
id,
pgp_sym_decrypt(email::bytea, 'db_encryption_key') AS email,
created_at
FROM users;لاحظ أننا استخدمنا pgp_sym_encrypt وpgp_sym_decrypt لتشفير وفك تشفير البيانات. المفتاح المستخدم هنا ('db_encryption_key') يجب أن يكون مخزناً في مكان آمن مثل AWS KMS أو HashiCorp Vault. أيضاً، يجب أن يكون هذا المفتاح مختلفاً عن مفتاح الـ JWT أو أي مفاتيح أخرى. لماذا؟ لأن إذا اخترق المهاجم قاعدة البيانات، لن يستطيع فك تشفير البيانات بدون هذا المفتاح. وإذا اخترق السيرفر، لن يستطيع الوصول إلى قاعدة البيانات بدون بيانات الاعتماد الخاصة بها.
في عام 2020، تعرضت شركة Shopify لهجوم DDoS استهدف الـ API الخاص بها. المهاجمون أرسلوا ملايين الطلبات في الدقيقة، مما تسبب في تعطل الخدمة لعدة ساعات. السبب؟ عدم وجود Rate Limiting فعال. المشكلة الأساسية في Rate Limiting هي أنها يمكن أن تكون إما ضعيفة جداً (لا تمنع الهجمات) أو قوية جداً (تخنق المستخدمين الشرعيين). الحل؟ استخدم نظام Rate Limiting ذكي يعتمد على الـ User Behavior وليس فقط على عدد الطلبات.
الطريقة التقليدية لـ Rate Limiting هي استخدام الـ Fixed Window أو الـ Sliding Window. في الـ Fixed Window، تحسب عدد الطلبات في فترة زمنية ثابتة (مثل دقيقة واحدة)، وإذا تجاوز العدد الحد المسموح، تمنع الطلبات الإضافية. المشكلة؟ المهاجمون يمكنهم إرسال كل الطلبات في آخر ثانية من النافذة، مما يسبب تحميلاً كبيراً على السيرفر. الحل الأفضل هو استخدام الـ Sliding Window مع الـ Token Bucket أو الـ Leaky Bucket. هذه الخوارزميات توفر توزيعاً أكثر سلاسة للطلبات، وتمنع الـ Burst Requests التي تسبب تحميلاً كبيراً على السيرفر.
// Sliding Window Rate Limiter using Redis
const redis = require('redis');
const client = redis.createClient();
async function isRateLimited(userId, windowSizeInSeconds, maxRequests) {
const key = `rate_limit:${userId}`;
const now = Date.now();
const windowStart = now - (windowSizeInSeconds * 1000);
// Remove requests older than the window
await client.zRemRangeByScore(key, 0, windowStart);
// Count remaining requests
const requestCount = await client.zCard(key);
if (requestCount >= maxRequests) {
return true; // Rate limited
}
// Add current request timestamp
await client.zAdd(key, [{ score: now, value: now.toString() }]);
await client.expire(key, windowSizeInSeconds);
return false; // Not rate limited
}
// Example usage
app.use(async (req, res, next) => {
const userId = req.user.id; // From JWT
const isLimited = await isRateLimited(userId, 60, 100); // 100 requests per minute
if (isLimited) {
return res.status(429).json({ error: "Too many requests" });
}
next();
});في هذا المثال، استخدمنا Redis لتنفيذ Rate Limiter باستخدام الـ Sliding Window. كل طلب يضاف إلى مجموعة مرتبة (Sorted Set) مع الـ Timestamp كقيمة. قبل إضافة الطلب، نحذف كل الطلبات التي خارج النافذة الزمنية الحالية. إذا كان عدد الطلبات المتبقية أكبر من الحد المسموح، نمنع الطلب. هذه الطريقة توفر توزيعاً أكثر عدالة للطلبات مقارنة بالـ Fixed Window. لكن ماذا عن الأداء؟ Redis سريع جداً ويمكنه التعامل مع آلاف العمليات في الثانية، لكن إذا كان لديك ملايين المستخدمين، قد تحتاج إلى استخدام نظام موزع مثل Redis Cluster.
المهاجمون لا يستخدمون دائماً أساليب بسيطة مثل إرسال آلاف الطلبات من نفس الـ IP. أحياناً يستخدمون أساليب أكثر ذكاءً مثل توزيع الطلبات على عدة IPs أو تغيير الـ User Agent مع كل طلب. الحل؟ استخدم Rate Limiting متكيف يعتمد على عدة عوامل مثل:
في شركة Twilio، يستخدمون نظام Rate Limiting متكيف يعتمد على الـ Machine Learning. النظام يتعلم السلوك الطبيعي لكل مستخدم ويحدد الحد الأقصى بناءً على ذلك. مثلاً، إذا كان المستخدم يرسل عادة 10 طلبات في الدقيقة، لكن فجأة بدأ يرسل 100 طلب، فإن النظام سيحد من طلباته تلقائياً. يمكنك تنفيذ شيء مشابه باستخدام مكتبات مثل express-rate-limit مع قواعد مخصصة:
const rateLimit = require('express-rate-limit');
// Adaptive rate limiter based on user behavior
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: (req) => {
// Allow more requests for authenticated users
if (req.user) {
return 100;
}
// Check if IP is in a suspicious list
if (isSuspiciousIp(req.ip)) {
return 10;
}
// Default limit
return 50;
},
message: "Too many requests, please try again later.",
keyGenerator: (req) => {
// Use both IP and User Agent to prevent spoofing
return `${req.ip}:${req.headers['user-agent']}`;
},
skip: (req) => {
// Skip rate limiting for certain endpoints
return req.path === '/health';
}
});
app.use(limiter);لاحظ أننا استخدمنا دالة مخصصة للـ max لتحديد الحد الأقصى بناءً على عدة عوامل. أيضاً، استخدمنا كلاً من الـ IP والـ User Agent كمفتاح لمنع المهاجمين من تغيير الـ User Agent لتجاوز الـ Rate Limiting. لكن حتى مع هذه الإجراءات، يبقى الـ Rate Limiting أداة دفاعية فقط. إذا كان المهاجم يستخدم شبكة كبيرة من الـ Proxies، قد تحتاج إلى استخدام خدمات مثل Cloudflare أو AWS WAF التي توفر حماية إضافية ضد الهجمات الكبيرة.
في عام 2017، تعرضت شركة Equifax لاختراق ضخم بسبب ثغرة في الـ API الخاص بها. المهاجمون استطاعوا تنفيذ هجوم SQL Injection وسرقوا بيانات 143 مليون مستخدم. السبب؟ استخدام الـ Raw SQL بدون تطهير المدخلات. الكثير من المطورين يعتقدون أن استخدام ORM مثل Sequelize أو TypeORM يحميهم تلقائياً من هجمات الحقن. الحقيقة؟ ORM يحمي فقط من الهجمات البسيطة، وليس من الهجمات المتقدمة مثل الـ Second-Order SQL Injection أو الـ NoSQL Injection.
الـ ORM يحول الاستعلامات إلى SQL آمن، لكنه لا يمنعك من كتابة استعلامات غير آمنة إذا استخدمت الـ Raw Queries. أيضاً، بعض الـ ORMs تسمح باستخدام الـ Raw SQL داخل الاستعلامات الآمنة، مما قد يفتح الباب لهجمات الحقن. الحل؟ دائماً استخدم الـ Parameterized Queries حتى مع الـ ORM. إليك مثال على كيفية استخدام Sequelize بشكل آمن:
// Safe query using Sequelize (ORM)
const { User } = require('./models');
// Safe: Using ORM methods
User.findAll({
where: {
email: req.query.email // Automatically sanitized by Sequelize
}
});
// Unsafe: Using raw queries without parameters
User.sequelize.query(
`SELECT * FROM users WHERE email = '${req.query.email}'`,
{ type: User.sequelize.QueryTypes.SELECT }
);
// Safe: Using parameterized raw queries
User.sequelize.query(
'SELECT * FROM users WHERE email = ?',
{ replacements: [req.query.email], type: User.sequelize.QueryTypes.SELECT }
);لاحظ أننا استخدمنا الـ ORM Method (findAll) الذي يطهر المدخلات تلقائياً. أيضاً، عندما اضطررنا لاستخدام الـ Raw Query، استخدمنا الـ Parameterized Query مع الـ replacements لضمان تطهير المدخلات. لكن ماذا عن هجمات NoSQL Injection؟ إذا كنت تستخدم MongoDB، حتى الـ ORM قد لا يحميك تماماً. إليك مثال على هجوم NoSQL Injection وكيفية منعه:
// Unsafe: Using user input directly in MongoDB query
app.get('/user', async (req, res) => {
const user = await db.collection('users').findOne({
email: req.query.email // Vulnerable to NoSQL Injection
});
res.json(user);
});
// Safe: Using parameterized query
app.get('/user', async (req, res) => {
// Validate and sanitize input
if (!validator.isEmail(req.query.email)) {
return res.status(400).json({ error: "Invalid email" });
}
const user = await db.collection('users').findOne({
email: req.query.email // Now safe
});
res.json(user);
});
// Example of NoSQL Injection attack
// Attacker sends: ?email[$ne]=invalid@example.com
// This returns the first user where email is not 'invalid@example.com'في المثال الأول، المهاجم يمكنه إرسال استعلام مثل ?email[$ne]=invalid@example.com والذي سيعيد أول مستخدم لا يساوي بريده الإلكتروني "invalid@example.com". الحل؟ دائماً قم بتطهير المدخلات باستخدام مكتبات مثل validator.js وتأكد من أن المدخلات تتبع التنسيق المتوقع قبل استخدامها في الاستعلامات.
في عام 2021، تعرضت شركة Codecov لاختراق استمر لعدة أشهر دون اكتشافه. المهاجمون استطاعوا تعديل سكربت CI/CD وسرقوا بيانات الاعتماد من آلاف الشركات. السبب؟ عدم وجود نظام مراقبة فعال. الحقيقة هي أن معظم الهجمات لا تُكتشف إلا بعد فوات الأوان. الحل؟ استخدم نظام مراقبة وتنبيهات ذكي يراقب السلوك غير الطبيعي في الوقت الفعلي.
المراقبة الفعالة تعتمد على ثلاثة عناصر رئيسية: السجلات (Logs)، المقاييس (Metrics)، والتنبيهات (Alerts). السجلات يجب أن تحتوي على معلومات كافية لإعادة بناء ما حدث، والمقاييس يجب أن توفر رؤية شاملة للأداء والأمان، والتنبيهات يجب أن تكون ذكية بما يكفي لتنبيهك فقط عندما يكون هناك تهديد حقيقي. إليك كيف تنفذ هذا النظام:
السجلات هي خط الدفاع الأول في اكتشاف الهجمات. لكن تسجيل كل شيء سيجعل السجلات غير قابلة للإدارة وسيؤثر على الأداء. الحل؟ استخدم سجلات منظمة (Structured Logging) وسجل فقط الأحداث المهمة. إليك ما يجب تسجيله:
استخدم مكتبات مثل Winston في Node.js أو Loguru في Python لتنظيم السجلات. أيضاً، استخدم خدمات مثل ELK Stack (Elasticsearch, Logstash, Kibana) أو Datadog لتخزين وتحليل السجلات. إليك مثال على كيفية تسجيل طلب إلى الـ API بشكل منظم:
const winston = require('winston');
const { combine, timestamp, json } = winston.format;
const logger = winston.createLogger({
level: 'info',
format: combine(timestamp(), json()),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'api.log' })
]
});
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
event: 'api_request',
method: req.method,
path: req.path,
status: res.statusCode,
duration,
ip: req.ip,
userAgent: req.headers['user-agent'],
userId: req.user?.id || null
});
});
next();
});المقاييس توفر رؤية شاملة للأداء والأمان. استخدم أدوات مثل Prometheus مع Grafana لجمع وعرض المقاييس. إليك بعض المقاييس المهمة التي يجب مراقبتها:
استخدم هذه المقاييس لإنشاء لوحات تحكم (Dashboards) تعرض حالة النظام في الوقت الفعلي. مثلاً، إذا لاحظت زيادة مفاجئة في عدد الأخطاء أو في وقت الاستجابة، قد يكون هذا علامة على هجوم DDoS أو مشكلة في السيرفر. إليك مثال على كيفية جمع المقاييس باستخدام Prometheus في Node.js:
const promClient = require('prom-client');
// Create metrics
const apiRequestsTotal = new promClient.Counter({
name: 'api_requests_total',
help: 'Total number of API requests',
labelNames: ['method', 'path', 'status']
});
const apiResp new promClient.Histogram({
name: 'api_response_time_seconds',
help: 'Response time of API requests in seconds',
labelNames: ['method', 'path'],
buckets: [0.1, 0.5, 1, 2, 5] // Buckets for response time
});
// Middleware to collect metrics
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000; // Convert to seconds
apiRequestsTotal.inc({
method: req.method,
path: req.path,
status: res.statusCode
});
apiResponseTime.observe({
method: req.method,
path: req.path
}, duration);
});
next();
});
// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});التنبيهات الزائدة تسبب التعب من التنبيهات (Alert Fatigue)، حيث يتجاهل المطورون التنبيهات لأنهم يتلقون الكثير منها. الحل؟ استخدم تنبيهات ذكية تعتمد على العتبات الديناميكية. مثلاً، بدلاً من تنبيهك عندما يتجاوز عدد الأخطاء 10 في الدقيقة، قم بتنبيهك عندما يتجاوز عدد الأخطاء المتوسط اليومي بعامل معين. إليك كيف تنفذ هذا باستخدام Prometheus:
# Prometheus alert rules
groups:
- name: api-alerts
rules:
- alert: HighErrorRate
expr: rate(api_requests_total{status=~"5.."}[1m]) > 5 * avg(rate(api_requests_total{status=~"5.."}[1m])[24h:])
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate on {{ $labels.path }}"
description: "Error rate is {{ $value }} errors per second (5x higher than 24h average)"
- alert: HighLatency
expr: histogram_quantile(0.95, sum(rate(api_response_time_seconds_bucket[5m])) by (le, path)) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "High latency on {{ $labels.path }}"
description: "95th percentile latency is {{ $value }} seconds"
- alert: SuspiciousLoginActivity
expr: rate(api_requests_total{path="/login", status="401"}[1m]) > 10
for: 1m
labels:
severity: critical
annotations:
summary: "Suspicious login activity"
description: "{{ $value }} failed login attempts per second in the last minute"في هذا المثال، استخدمنا تعبيرات PromQL لتنبيهنا عندما يتجاوز معدل الأخطاء 5 أضعاف المتوسط اليومي، أو عندما يتجاوز وقت الاستجابة الـ 95th percentile ثانيتين، أو عندما يتجاوز عدد محاولات تسجيل الدخول الفاشلة 10 في الدقيقة. هذه التنبيهات ذكية لأنها تتكيف مع سلوك النظام الطبيعي، بدلاً من استخدام عتبات ثابتة قد تسبب تنبيهات زائفة.
الأمان ليس طبقة تضاف في النهاية، بل هو جزء أساسي من تصميم الـ API. إليك الخلاصة العملية التي أستخدمها في كل مشروع:
الخطوة التالية؟ ابدأ بتطبيق هذه الإجراءات على الـ API الخاص بك اليوم. ابدأ بأصغر نقطة نهاية واختبر الأداء والأمان. تذكر أن الأمان هو عملية مستمرة، وليس هدفاً نهائياً. ابقَ على اطلاع بأحدث التهديدات واستمر في تحسين نظامك. وإذا كنت تريد تحدياً حقيقياً، حاول اختراق الـ API الخاص بك باستخدام أدوات مثل OWASP ZAP أو Burp Suite. فقط عندما تستطيع اختراقه، ستعرف كيف تحميه.