تأمين API ليس مجرد إضافة مفتاح في الهيدر. اكتشف كيف تبني طبقات حماية حقيقية من المصادقة المتعددة العوامل إلى الحد من الطلبات، مع أمثلة عملية وكود جاهز للتطبيق في الإنتاج.
في آخر مراجعة أمنية لمشروع عملاق كان يستضيف ١٢ مليون مستخدم نشط، اكتشفنا ثغرة بسيطة في الـ API: مفتاح واحد ثابت في الكود يسمح لأي شخص باستخراج بيانات المستخدمين بالكامل. لم يكن الخطأ في المنطق البرمجي، بل في افتراض أن الـ Authentication وحده يكفي. الحقيقة هي أن تأمين API يشبه بناء قلعة: المصادقة هي البوابة، لكن الأسوار والأبراج هي الـ Rate Limiting، الـ Input Validation، والـ Encryption. إذا تجاهلت طبقة واحدة، ستجد المهاجمين داخل القصر قبل أن تفتح عينيك.
في هذا الدليل، لن نتحدث عن النظريات. سنبدأ من الصفر ونبني نظام حماية حقيقي باستخدام أدوات حديثة مثل JWT، OAuth2، وRedis للـ Rate Limiting. سأريك كيف تعمل كل طبقة خلف الكواليس، أين تقع الفخاخ الشائعة، وكيف تتجنبها. الكود الذي ستراه هنا مستخدم بالفعل في بيئات إنتاجية، وليس مجرد أمثلة تعليمية.
الكثير من المطورين يعتقدون أن إضافة `Authorization: Bearer {token}` في الهيدر كافية. لكن هذا التفكير سطحي وخطير. المصادقة الحقيقية تبدأ بفهم أنواع الـ Tokens وكيفية إدارتها. مثلاً، الـ API Keys جيدة للخدمات الداخلية، لكنها كارثة إذا استخدمت مع المستخدمين النهائيين. لماذا؟ لأن المفتاح الثابت يمكن سرقته بسهولة، ولا يوفر أي معلومات عن هوية المستخدم أو صلاحياته.
بدلاً من ذلك، استخدم JWT (JSON Web Tokens) مع الـ Claims المخصصة. الـ JWT يسمح لك بتخزين معلومات مثل `userId`، `role`، وحتى `expiry` داخل الـ Token نفسه. لكن احذر: الـ JWT ليس سحرياً. إذا لم تستخدمه بشكل صحيح، ستجد نفسك أمام مشاكل مثل الـ Token Hijacking أو الـ Replay Attacks. مثلاً، في أحد المشاريع، اكتشفنا أن المطورين كانوا يرسلون الـ JWT في الـ URL بدلاً من الهيدر، مما يجعله ظاهراً في سجلات الـ Server وذاكرة الـ Browser.
// مثال على توليد JWT آمن مع Claims مخصصة
const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET; // يجب أن يكون سرياً ومعقداً
function generateToken(user) {
return jwt.sign(
{
sub: user.id,
role: user.role,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 * 60) // ساعة واحدة
},
secret,
{ algorithm: 'HS256' } // لا تستخدم أبداً HS256 في الإنتاج بدون HTTPS
);
}
// التحقق من الـ Token
function verifyToken(token) {
try {
const decoded = jwt.verify(token, secret);
return decoded;
} catch (err) {
throw new Error('Invalid or expired token');
}
}لاحظ هنا أننا استخدمنا `sub` بدلاً من `userId` مباشرة، وهذا يتبع معيار RFC 7519. أيضاً، الـ `exp` يحدد صلاحية الـ Token، مما يقلل من خطر استخدامه إذا سُرق. لكن تذكر: الـ JWT لا يحمي البيانات، بل يحمي الهوية. إذا كان الـ Token يحتوي على معلومات حساسة، استخدم الـ JWE (JSON Web Encryption) بدلاً من الـ JWS (JSON Web Signature).
OAuth2 ليس بديلاً عن JWT، بل هو بروتوكول تفويض (Authorization) وليس مصادقة (Authentication). كثيراً ما يختلط الأمر على المطورين. مثلاً، عندما تريد السماح لموقع خارجي بالوصول إلى بيانات المستخدم على فيسبوك، تستخدم OAuth2. لكن إذا كنت تبني API داخلي لتطبيقك، فقد يكون JWT كافياً. الفرق الأساسي هو أن OAuth2 يوفر تدفقاً آمناً للحصول على الـ Access Token دون كشف بيانات الاعتماد الأصلية.
في أحد المشاريع، استخدمنا OAuth2 مع Grant Type `authorization_code` للسماح لتطبيقات الطرف الثالث بالوصول إلى بيانات المستخدمين. لكن واجهنا مشكلة كبيرة: الـ Redirect URIs. إذا لم تُتحقق منها بدقة، يمكن للمهاجم أن يخدع المستخدم للدخول إلى رابط مزيف ويحصل على الـ Authorization Code. الحل؟ استخدم مكتبة موثوقة مثل `passport-oauth2` في Node.js، وتأكد من التحقق من الـ Redirect URI على السيرفر قبل إرسال الـ Code.
// مثال على تحقق من Redirect URI في OAuth2
const crypto = require('crypto');
function validateRedirectUri(clientRedirectUri, allowedUris) {
// أولاً، تحقق من تطابق البروتوكول (HTTPS فقط في الإنتاج)
if (!clientRedirectUri.startsWith('https://')) {
return false;
}
// تحقق من تطابق النطاق تماماً
const clientHost = new URL(clientRedirectUri).host;
return allowedUris.some(uri => new URL(uri).host === clientHost);
}
// مثال على توليد Authorization Code
function generateAuthCode() {
return crypto.randomBytes(32).toString('hex');
}بعد أن يتأكد الـ API من هوية المستخدم، يأتي دور التفويض: ماذا يسمح له بفعله؟ الكثير من المطورين يعتمدون على الـ Roles البسيطة مثل `admin`، `user`، لكن هذا النهج لا يكفي في التطبيقات المعقدة. مثلاً، في نظام إدارة المحتوى، قد يحتاج المستخدم إلى صلاحية تعديل المقالات التي يملكها فقط، وليس كل المقالات. هذا ما يسمى بـ Attribute-Based Access Control (ABAC).
في أحد المشاريع، استخدمنا مكتبة `casl` في Node.js لتنفيذ ABAC. بدلاً من التحقق من الدور فقط، نتحقق من سمات المستخدم والمورد. مثلاً، هل المستخدم يملك المقالة؟ هل المقالة منشورة بالفعل؟ هل المستخدم لديه صلاحية التحرير؟ هذا النهج يجعل الكود أكثر مرونة وأقل عرضة للأخطاء. لكن احذر: الـ ABAC يمكن أن يصبح معقداً جداً إذا لم تخطط له جيداً. ابدأ دائماً بالصلاحيات الأساسية، ثم أضف التعقيد تدريجياً.
// مثال على ABAC باستخدام مكتبة casl
const { AbilityBuilder, Ability } = require('@casl/ability');
function defineAbilitiesFor(user) {
const { can, cannot, build } = new AbilityBuilder(Ability);
// المستخدم العادي يمكنه قراءة المقالات
can('read', 'Article');
// إذا كان المستخدم يملك المقالة، يمكنه تعديلها
can('update', 'Article', { authorId: user.id });
// المدير يمكنه حذف أي مقالة
if (user.role === 'admin') {
can('delete', 'Article');
}
return build();
}
// استخدام الـ Ability في الـ API
function canUserUpdateArticle(user, article) {
const ability = defineAbilitiesFor(user);
return ability.can('update', article);
}لاحظ كيف أن الـ ABAC يسمح لنا بتحديد شروط دقيقة جداً. بدلاً من التحقق من `user.role === 'admin'` في كل مكان في الكود، نحدد القواعد مرة واحدة ونستخدمها في كل مكان. هذا يجعل الكود أكثر نظافة وأسهل للصيانة. لكن تذكر: الـ ABAC ليس بديلاً عن التحقق من الصلاحيات في قاعدة البيانات. دائماً تحقق من أن المستخدم يملك المورد فعلياً قبل السماح له بالعمل عليه.
في أحد المشاريع، اكتشفنا أن المطورين كانوا يمنحون صلاحيات أكثر مما يحتاجه المستخدم. مثلاً، كان الـ API يسمح لأي مستخدم لديه دور `editor` بتعديل أي مقالة، وليس فقط المقالات التي يملكها. هذا خطأ شائع يحدث عندما يعتمد المطورون على الـ Roles فقط دون التفكير في السياق. الحل؟ استخدم دائماً مبدأ أقل صلاحية (Principle of Least Privilege). امنح المستخدم أقل صلاحية ممكنة لإنجاز المهمة، ثم أضف المزيد إذا لزم الأمر.
في عام ٢٠١٨، تعرضت GitHub لهجوم DDoS ضخم باستخدام تقنية تسمى Memcached Amplification. المهاجمون أرسلوا طلبات صغيرة إلى سيرفرات Memcached مكشوفة، والتي ردت بردود ضخمة، مما أدى إلى تحميل زائد على GitHub. لو كان لديهم نظام Rate Limiting فعال، لكان بإمكانهم تخفيف الهجوم بشكل كبير. الـ Rate Limiting ليس مجرد أداة لمنع الإساءة، بل هو طبقة حماية أساسية ضد الهجمات.
هناك عدة طرق لتنفيذ الـ Rate Limiting، لكن الأكثر شيوعاً هي Token Bucket وFixed Window. الـ Token Bucket يسمح بتجاوز الحد لفترة قصيرة، بينما الـ Fixed Window يفرض حداً صارماً في فترة زمنية محددة. في معظم الحالات، الـ Token Bucket هو الخيار الأفضل لأنه أكثر مرونة. لكن كيف تخزن عدد الطلبات؟ هنا يأتي دور Redis. Redis سريع جداً في العمليات الذرية مثل INCR وEXPIRE، مما يجعله مثالياً للـ Rate Limiting.
// Rate Limiting باستخدام Redis و Token Bucket
const redis = require('redis');
const client = redis.createClient();
async function rateLimit(ip, limit = 100, window = 60) {
const key = `rate_limit:${ip}`;
const now = Date.now();
const windowInMs = window * 1000;
// احصل على البيانات الحالية من Redis
const data = await client.hGetAll(key);
// إذا لم يكن هناك بيانات، ابدأ من الصفر
if (!data.tokens) {
await client.hSet(key, {
tokens: limit - 1,
lastRefill: now
});
await client.expire(key, window);
return { allowed: true, remaining: limit - 1 };
}
// حساب عدد الـ Tokens المتاحة
const timePassed = now - parseInt(data.lastRefill);
const tokensToAdd = Math.floor(timePassed / (windowInMs / limit));
const newTokens = Math.min(parseInt(data.tokens) + tokensToAdd, limit);
// إذا لم يكن هناك tokens كافية، ارفض الطلب
if (newTokens < 1) {
return { allowed: false, remaining: 0 };
}
// تحديث البيانات في Redis
await client.hSet(key, {
tokens: newTokens - 1,
lastRefill: now - (timePassed % (windowInMs / limit))
});
return { allowed: true, remaining: newTokens - 1 };
}هذا الكود يستخدم خوارزمية Token Bucket مع Redis. لاحظ كيف أننا نعيد حساب الـ Tokens بناءً على الوقت المنقضي منذ آخر طلب. هذا يسمح لنا بالسماح بتجاوز الحد لفترة قصيرة إذا كان هناك عدد قليل من الطلبات مؤخراً. أيضاً، نستخدم `hSet` بدلاً من `set` لتخزين البيانات، مما يسمح لنا بتخزين عدة حقول في مفتاح واحد. هذا يجعل الكود أكثر كفاءة ويقلل من عدد العمليات على Redis.
عندما يتجاوز المستخدم الحد، لا تكتفِ بإرسال رسالة خطأ. استخدم الـ HTTP Status Code المناسب: `429 Too Many Requests`. أيضاً، أضف هيدرات مفيدة مثل `Retry-After` لتخبر العميل متى يمكنه المحاولة مرة أخرى. في أحد المشاريع، استخدمنا هذه الاستراتيجية مع واجهة أمامية (Frontend) تعرض رسالة للمستخدم تخبره بأنه تجاوز الحد، وتخبره متى يمكنه المحاولة مرة أخرى. هذا يحسن تجربة المستخدم ويقلل من الضغط على السيرفر.
// مثال على رد عند تجاوز الحد
function rateLimitExceededResponse(retryAfter) {
return {
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': retryAfter // بالثواني
},
body: JSON.stringify({
error: 'Rate limit exceeded',
message: `You have exceeded the request limit. Please try again in ${retryAfter} seconds.`,
retryAfter
})
};
}الـ Encryption ليس مجرد إضافة `https://` في بداية الرابط. في أحد المشاريع، اكتشفنا أن المطورين كانوا يرسلون بيانات حساسة مثل كلمات المرور في الـ Query Parameters. هذا خطأ فادح لأن الـ Query Parameters تظهر في سجلات السيرفر وذاكرة المتصفح. بدلاً من ذلك، استخدم دائماً الـ Body للبيانات الحساسة، وتأكد من تشفيرها باستخدام TLS 1.2 أو أعلى.
لكن الـ Encryption لا ينتهي عند الـ Transport Layer. يجب أيضاً تشفير البيانات في قاعدة البيانات (At Rest). مثلاً، استخدم الـ Transparent Data Encryption (TDE) في SQL Server أو الـ Encryption at Rest في MongoDB. أيضاً، لا تخزن كلمات المرور أبداً كنص عادي. استخدم دوماً خوارزميات التجزئة القوية مثل bcrypt أو Argon2. في أحد المشاريع، استخدمنا bcrypt مع Salt مخصص لكل مستخدم، مما جعل من الصعب جداً على المهاجمين استخدام جداول قوس قزح (Rainbow Tables) لكسر كلمات المرور.
// مثال على تجزئة كلمة المرور باستخدام bcrypt
const bcrypt = require('bcrypt');
const saltRounds = 12; // كلما زاد العدد، زادت الأمان (لكن أبطأ)
async function hashPassword(password) {
return await bcrypt.hash(password, saltRounds);
}
async function comparePasswords(password, hash) {
return await bcrypt.compare(password, hash);
}لاحظ أن bcrypt بطيء عن قصد. هذا يجعل من الصعب على المهاجمين تجربة ملايين كلمات المرور في الثانية. أيضاً، الـ Salt يتم توليده تلقائياً لكل كلمة مرور، مما يجعل الهجمات باستخدام Rainbow Tables غير فعالة. لكن تذكر: الـ Encryption ليس حلاً سحرياً. إذا كان المهاجم لديه وصول إلى السيرفر، يمكنه سرقة البيانات قبل تشفيرها. لهذا السبب يجب أيضاً حماية الوصول إلى السيرفر نفسه باستخدام آليات مثل الـ Firewalls، الـ VPNs، والـ Multi-Factor Authentication.
في بعض الحالات، قد تحتاج إلى تشفير البيانات حتى في الـ API Responses. مثلاً، إذا كنت ترسل بيانات طبية حساسة، قد تحتاج إلى تشفير الـ Payload باستخدام JWE (JSON Web Encryption). هذا يضيف طبقة حماية إضافية حتى إذا تم اعتراض الـ Request. لكن احذر: الـ JWE يزيد من حجم البيانات ويجعل المعالجة أبطأ. استخدمه فقط عندما تكون البيانات حساسة جداً.
// مثال على توليد JWE باستخدام مكتبة jose
const { JWE, JWK } = require('jose');
async function encryptData(data, publicKey) {
const jwe = new JWE.Encrypt(JSON.stringify(data));
jwe.recipient(publicKey);
jwe.update(JSON.stringify(data));
return jwe.encrypt('A256GCM');
}
async function decryptData(jweToken, privateKey) {
const { plaintext } = await JWE.decrypt(jweToken, privateKey);
return JSON.parse(plaintext.toString());
}في عام ٢٠١٧، تعرضت Equifax لخرق بيانات ضخم بسبب ثغرة في الـ API تسمح بحقن SQL. السبب؟ لم يتم التحقق من مدخلات المستخدم بشكل صحيح. الـ Input Validation ليس مجرد خطوة إضافية، بل هو خط دفاع أساسي ضد الهجمات مثل SQL Injection، XSS، وCSRF. لكن الكثير من المطورين يعتمدون على الـ Frontend للتحقق من المدخلات، وهذا خطأ فادح لأن المهاجمين يمكنهم تجاوز الـ Frontend بسهولة.
استخدم دوماً مكتبات موثوقة للتحقق من المدخلات مثل `joi` في Node.js أو `pydantic` في Python. مثلاً، في أحد المشاريع، استخدمنا `joi` للتحقق من أن الـ Email يحتوي على حرف `@` وأن الـ Password يحتوي على الأقل على ٨ أحرف. أيضاً، استخدمنا `sanitize-html` لتنظيف المدخلات التي ستعرض في الـ Frontend لمنع XSS. لكن تذكر: الـ Sanitization ليس بديلاً عن الـ Validation. دائماً تحقق أولاً، ثم نظف.
// مثال على Input Validation باستخدام joi
const Joi = require('joi');
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(),
age: Joi.number().integer().min(18).max(120),
role: Joi.string().valid('user', 'admin')
});
async function validateUserInput(input) {
try {
const value = await userSchema.validateAsync(input);
return { valid: true, value };
} catch (err) {
return { valid: false, error: err.details[0].message };
}
}لاحظ كيف أننا نتحقق من أن الـ Email يحتوي على `@`، وأن الـ Password يحتوي على أحرف وأرقام فقط، وأن الـ Age بين ١٨ و١٢٠. أيضاً، نحدد القيم المسموح بها للـ `role`. هذا يمنع المهاجمين من إدخال قيم غير متوقعة. لكن تذكر: الـ Validation ليس بديلاً عن الـ Sanitization. حتى إذا كانت المدخلات صحيحة، قد تحتوي على أكواد ضارة إذا كانت ستعرض في الـ Frontend.
في أحد المشاريع، اكتشفنا أن المطورين كانوا يرسلون بيانات المستخدم مباشرة من قاعدة البيانات إلى الـ Frontend دون تنظيف. هذا سمح للمهاجمين بحقن أكواد JavaScript ضارة في الـ API Responses، مما أدى إلى هجمات XSS. الحل؟ استخدم دوماً مكتبات مثل `sanitize-html` لتنظيف البيانات قبل إرسالها. مثلاً، إذا كان المستخدم يدخل تعليقاً يحتوي على `>alert('xss')</script>`، يجب إزالة الـ `<script>` قبل عرضه.
// مثال على Sanitization باستخدام sanitize-html
const sanitizeHtml = require('sanitize-html');
function sanitizeUserInput(input) {
return sanitizeHtml(input, {
allowedTags: ['b', 'i', 'em', 'strong', 'a'],
allowedAttributes: {
'a': ['href']
},
allowedIframeHostnames: [] // لا تسمح بأي iframes
});
}في أحد المشاريع، تعرضنا لهجوم Brute Force على الـ API. لم نكتشفه إلا بعد يومين لأننا لم نكن نرصد محاولات تسجيل الدخول الفاشلة. الـ Monitoring ليس مجرد أداة لتحسين الأداء، بل هو جزء أساسي من الأمان. استخدم أدوات مثل ELK Stack (Elasticsearch, Logstash, Kibana) أو Grafana لمراقبة الـ API في الوقت الفعلي. مثلاً، يمكنك إعداد تنبيهات عندما يتجاوز عدد الطلبات حداً معيناً، أو عندما يكون هناك عدد كبير من محاولات تسجيل الدخول الفاشلة.
لكن الـ Monitoring وحده لا يكفي. يجب أيضاً تحليل السجلات (Logs) بانتظام للبحث عن أنماط مشبوهة. مثلاً، إذا لاحظت أن هناك طلبات متكررة من نفس الـ IP إلى نفس الـ Endpoint، قد يكون هذا محاولة هجوم. أيضاً، استخدم أدوات مثل AWS WAF أو Cloudflare لحماية الـ API من الهجمات الشائعة مثل SQL Injection وXSS. لكن تذكر: لا تعتمد فقط على الأدوات. دائماً قم بمراجعة السجلات يدوياً بين الحين والآخر للبحث عن أي نشاط غير عادي.
// مثال على تسجيل محاولات تسجيل الدخول باستخدام Winston
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'logs/auth.log' })
]
});
function logAuthAttempt(ip, username, success) {
logger.info({
timestamp: new Date().toISOString(),
ip,
username,
success,
userAgent: process.env.USER_AGENT
});
}
// مثال على استخدام في الـ API
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const ip = req.ip;
// تحقق من كلمة المرور...
if (success) {
logAuthAttempt(ip, username, true);
res.json({ token: generateToken(user) });
} else {
logAuthAttempt(ip, username, false);
res.status(401).json({ error: 'Invalid credentials' });
}
});لاحظ كيف أننا نسجل الـ IP، اسم المستخدم، ونتيجة المحاولة. هذا يسمح لنا بتحليل السجلات لاحقاً للبحث عن أنماط مشبوهة. أيضاً، نستخدم `winston` بدلاً من `console.log` لأنه أكثر مرونة ويسمح لنا بتخزين السجلات في ملفات أو إرسالها إلى خدمات خارجية مثل Elasticsearch. لكن تذكر: السجلات نفسها قد تحتوي على بيانات حساسة. دائماً تأكد من تشفير السجلات أو تخزينها في مكان آمن.
تأمين API ليس مهمة تقوم بها مرة واحدة وتنتهي. إنه عملية مستمرة تتطلب مراقبة وتحديث مستمرين. ابدأ دائماً بالمصادقة القوية (JWT أو OAuth2)، ثم أضف طبقات الحماية الأخرى مثل الـ Rate Limiting، الـ Input Validation، والـ Encryption. استخدم أدوات موثوقة ولا تعتمد على الكود الخاص بك فقط. مثلاً، استخدم مكتبات مثل `passport` للمصادقة، `casl` للتفويض، و`joi` للتحقق من المدخلات.
أيضاً، لا تنسَ اختبار الأمان بانتظام. استخدم أدوات مثل OWASP ZAP أو Burp Suite للبحث عن الثغرات. وفي النهاية، تذكر أن الأمان ليس مسؤولية فريق معين. كل مطور في الفريق يجب أن يفهم أساسيات الأمان وكيفية تطبيقها. إذا وجدت ثغرة، أصلحها فوراً ولا تنتظر حتى يتسبب الأمر في كارثة. الأمان يبدأ منك.