أصبحت رموز الويب JSON (JWTs) المعيار الفعلي لتأمين واجهات برمجة التطبيقات وإدارة مصادقة المستخدمين في تطبيقات الويب الحديثة. بينما تقدم JWTs جاذبية البنية غير المرتبطة بالحالة—حيث لا يحتاج الخادم إلى تخزين بيانات الجلسة—هذه الميزة تثير تحديات أمنية كبيرة. على وجه التحديد، إذا تم سرقة رمز JWT، فإنه يظل صالحًا حتى ينتهي صلاحيته. يستكشف هذا المنشور استراتيجيات قوية لدوران رموز JWT وإلغاء صلاحيتها، ويقارن بين كيفية عمل هذه الآليات في كل من البيئات المرتبطة وغير المرتبطة بالحالة.
المشكلة الأساسية: سرقة الرموز وانتهاء الصلاحية
في تنفيذ JWT الأساسي، بمجرد إصدار الرمز، يثق الخادم بتوقيعه حتى يتم الوصول إلى مطالبة exp (انتهاء الصلاحية). إذا قام المهاجم باعتراض هذا الرمز، فسيحصل على وصول غير مصرح به حتى يغلق هذا النافذة الزمنية. للتخفيف من هذا، يجب علينا تنفيذ دوران الرموز (إصدار رموز جديدة) وإلغاء الصلاحية (إبطال الرموز مبكرًا).
الاستراتيجية 1: النهج غير المرتبط بالحالة باستخدام رموز وصول قصيرة العمر
في البنية غير المرتبطة بالحالة تمامًا، لا يمكن للخادم التحقق من قاعدة بيانات مركزية لمعرفة ما إذا كان الرمز قد تم إلغاؤه. لذلك، يتمثل الدفاع الأساسي في تقليل فرصة المهاجمين. نحقق ذلك باستخدام رموز وصول قصيرة العمر مقترنة برموز تحديث طويلة العمر.
كيف يعمل:
- يتلقى العميل رمز وصول قصير العمر (على سبيل المثال، 15 دقيقة).
- عند انتهاء صلاحية رمز الوصول، يستخدم العميل رمز التحديث للحصول على رمز وصول جديد.
- يتم تخزين رمز التحديث بشكل آمن (على سبيل المثال، كوكي HttpOnly) ولديه عمر أطول (على سبيل المثال، 7 أيام).
حتى في الإعداد غير المرتبط بالحالة، يمكننا تنفيذ إلغاء صلاحية بسيط من خلال التحقق من "قائمة الرموز السوداء" أو طابع زمني "آخر نشاط" مخزن في متجر مفتاح-قيمة سريع مثل Redis. بينما يؤدي هذا إلى اعتماد طفيف على الحالة، إلا أنه ضئيل مقارنة بإدارة الجلسات التقليدية.
// كود وهمي للتحقق من JWT مع فحص سريع لـ Redis
async function verifyToken(token, redisClient) {
const decoded = jwt.verify(token, SECRET_KEY);
// التحقق مما إذا كان معرف الرمز موجودًا في قائمة سوداء قصيرة الأمد أو تم تحديثه
const tokenRecord = await redisClient.get(`token:${decoded.jti}`);
if (tokenRecord === 'revoked') {
throw new Error('Token revoked');
}
// اختياري: التحقق مما إذا كان "آخر تغيير لكلمة المرور" للمستخدم أحدث من إصدار الرمز
const user = await getUser(decoded.sub);
if (decoded.iat < user.lastPasswordChange) {
throw new Error('Token issued before password change');
}
return decoded;
}
الاستراتيجية 2: النهج المرتبط بالحالة باستخدام جلسات جانب الخادم
في البنية المرتبطة بالحالة، يحافظ الخادم على سجل للجلسات النشطة. يوفر هذا تحكمًا دقيقًا في صلاحية الرموز ولكنه يتطلب بنية تحتية أكثر. هنا، يمكن استخدام JWTs لا تزال كحامل للمطالبات، لكن الخادم يتحقق منها مقابل قاعدة بيانات للجلسات النشطة.
آليات إلغاء الصلاحية:
- تسجيل الخروج: عندما يقوم المستخدم بتسجيل الخروج، يحذف الخادم سجل الجلسة من قاعدة البيانات. أي طلبات مستقبلية باستخدام ذلك JWT ستفشل في التحقق لأن الخادم لا يمكنه العثور على الجلسة المرتبطة.
- تسجيل خروج قسري: يمكن للخادم حذف جميع الجلسات لمعرف مستخدم معين، مما يؤدي إلى إبطال جميع الرموز النشطة عبر جميع الأجهزة على الفور.
التنفيذ: إدراج الرموز في القائمة السوداء في Node.js
سواء كانت البنية مرتبطة بالحالة أو غير مرتبطة بها (مع حالة طفيفة)، فإن تنفيذ قائمة سوداء أمر حاسم لإلغاء الصلاحية الفوري. فيما يلي مثال على كيفية إدراج JWT في القائمة السوداء باستخدام Node.js وRedis. نحن نخزن مطالبة jti (معرف JWT) كمفتاح ونحدد وقت انتهاء الصلاحية مساويًا للصلاحية المتبقية للرمز.
const jwt = require('jsonwebtoken');
const redis = require('redis');
const redisClient = redis.createClient();
// إدراج رمز في القائمة السوداء عند تسجيل الخروج
async function blacklistToken(req, res) {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token); // فك التشفير دون التحقق للحصول على jti
if (!decoded || !decoded.jti) {
return res.status(400).send('Invalid token structure');
}
// حساب TTL بناءً على وقت انتهاء صلاحية الرمز
const now = Math.floor(Date.now() / 1000);
const ttl = decoded.exp - now;
// تخزين jti في Redis مع وقت انتهاء الصلاحية
await redisClient.setex(`blacklist:${decoded.jti}`, ttl, 'revoked');
res.status(200).send('Successfully logged out');
}
// وسيطة للتحقق من القائمة السوداء أثناء التحقق من الطلب
async function checkBlacklist(req, res, next) {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);
if (decoded && decoded.jti) {
const isBlacklisted = await redisClient.get(`blacklist:${decoded.jti}`);
if (isBlacklisted) {
return res.status(401).send('Token has been revoked');
}
}
next();
}
أفضل الممارسات للدوران الآمن
- استخدام معرفات فريدة: قم دائمًا بتضمين مطالبة
jtiفريدة في رموز JWT الخاصة بك. هذا يسمح بالتحديد الدقيق للرموز لأغراض إلغاء الصلاحية. - دوران رموز التحديث: كلما تم تحديث رمز الوصول، قم بإصدار رمز تحديث جديد وإبطال القديم. يمنع هذا هجمات إعادة التشغيل إذا تم سرقة رمز التحديث بعد الاستخدام.
- التخزين الآمن: لا تخزن أبدًا رموز JWT في
localStorageإذا كان تطبيقك عرضة لهجمات XSS. استخدم كوكيHttpOnlyوSecureبدلاً من ذلك للتخفيف من مخاطر البرمجة النصية عبر المواقع. - مراقبة الشذوذ: قم بتنفيذ تسجيل المراقبة والكشف عن أنماط استخدام الرموز غير العادية، مثل تسجيل الدخول المتزامن المتعدد من مواقع جغرافية مختلفة.
الخاتمة
يتطلب تنفيذ دوران وإلغاء صلاحية JWT الآمن موازنة بين الأمان والأداء. تعتمد البنى غير المرتبطة بالحالة بشكل كبير على أعمار رموز قصيرة وطبقات تخزين مؤقت فعالة (مثل Redis) للتعامل مع إلغاء الصلاحية دون تخزين جلسات كاملة. توفر البنى المرتبطة بالحالة إلغاء صلاحية أسهل على حساب زيادة ذاكرة الخادم وحمل قاعدة البيانات. بالنسبة لمعظم التطبيقات الحديثة، يوفر النهج الهجين—باستخدام رموز وصول قصيرة العمر، وتدوير رموز التحديث، واستخدام متجر سريع للمفتاح-القيمة لإدراج الرموز في القائمة السوداء—أفضل توازن بين الأمان والقابلية للتوسع وتجربة المستخدم.