NestJS — Node.js للمؤسسات

رموز التحديث والجلسات الآمنة

18 دقيقة الدرس 28 من 30

رموز التحديث والجلسات الآمنة

رموز الوصول قصيرة العمر آمنة لكنها غير مريحة — رمز بـ 15 دقيقة يعني خروج المستخدم كل 15 دقيقة. تحلّ رموز التحديث (Refresh tokens) هذا: رمز طويل العمر يستطيع سكّ رموز وصول جديدة دون إعادة إدخال كلمة المرور. وإتقان هذا النمط هو ما يجعل نظام مصادقة الإنتاج آمنًا وقابلًا للاستخدام معًا.

نمط الرمزين

  • رمز الوصول — قصير العمر (مثلًا 15 دقيقة)، يُرسَل في كل طلب، JWT عديم الحالة.
  • رمز التحديث — طويل العمر (مثلًا 7 أيام)، يُستخدم فقط للحصول على رمز وصول جديد، ويُخزَّن في الخادم ليمكن إبطاله.

حين ينتهي رمز الوصول، يستدعي العميل نقطة /refresh برمز التحديث ويستلم رمز وصول جديدًا — دون كلمة مرور.

إصدار كليهما عند الدخول

async login(user: { id: number }) { const payload = { sub: user.id }; const accessToken = this.jwt.sign(payload, { expiresIn: '15m' }); const refreshToken = this.jwt.sign(payload, { expiresIn: '7d' }); // خزّن تجزئة رمز التحديث، لا الرمز الخام أبدًا await this.users.setRefreshHash(user.id, await bcrypt.hash(refreshToken, 10)); return { accessToken, refreshToken }; }
خزّن تجزئة رمز التحديث فقط، لا القيمة الخام أبدًا. إن تسرّبت قاعدة بياناتك، لا يستطيع مهاجم استخدام رموز تحديث مُجزّأة — تمامًا كـ كلمات المرور. قارن الرمز الوارد بالتجزئة المخزّنة عند التحديث.

نقطة التحديث

عند التحديث، تحقّق من توقيع رمز التحديث، وأكّد مطابقته للتجزئة المخزّنة لذلك المستخدم، ثم أصدِر رموزًا جديدة:

async refresh(userId: number, presentedToken: string) { const user = await this.users.findById(userId); const valid = user?.refreshHash && (await bcrypt.compare(presentedToken, user.refreshHash)); if (!valid) throw new UnauthorizedException(); return this.login(user); // أصدِر زوجًا جديدًا (ودوّر رمز التحديث) }

تدوير رمز التحديث

دوّر عند كل تحديث. في كل مرّة يُستخدَم فيها رمز تحديث، أصدِر رمز تحديث جديدًا وأبطِل القديم. إن أُعيد تشغيل رمز مسروق بعد أن دوّر المستخدم الحقيقي أصلًا، صار إعادة الاستخدام قابلة للكشف — إشارة قوية لإبطال الجلسة كلّها.

أين تُخزَّن الرموز على العميل

اختيار التخزين مهمّ للأمان:

  • كوكي httpOnly — غير قابل للقراءة من JavaScript، فيقاوم سرقة الرمز عبر XSS. مُفضَّل لرمز التحديث (مع حماية CSRF).
  • الذاكرة — يمكن أن يعيش رمز الوصول في الذاكرة؛ يُفقَد عند التحديث، وهذا مقبول لأنّه قصير العمر.
  • localStorage — مريح لكن يقرؤه أي سكربت، فهو عرضة لـ XSS. تجنّبه للرموز طويلة العمر.

تسجيل الخروج

لأنّ رموز الوصول عديمة الحالة، يعني "تسجيل الخروج" فعليًا إبطال رمز التحديث في الخادم: امسح تجزئة التحديث المخزّنة فلا يعود يسكّ رموز وصول جديدة. ثم ينتهي رمز الوصول قصير العمر من تلقاء نفسه.

هذه مقايضة المصادقة عديمة الحالة. لا تستطيع قتل رمز وصول حيّ فورًا، لكن بانتهاء قصير ورمز تحديث قابل للإبطال ومُجزّأ ومُدوَّر، تحصل على نظام جلسات آمن وعملي.

الخلاصة

اقرن رمز وصول قصير العمر برمز تحديث طويل العمر مخزّن في الخادم. خزّن تجزئة رمز التحديث فقط، وتحقّق منه ودوّره في كل /refresh، وفضّل كوكي httpOnly له، و"سجّل الخروج" بمسح تجزئة التحديث المخزّنة. هذا يحقّق الأمان (رموز وصول قصيرة، تحديث قابل للإبطال) والاستخدامية (بلا إعادة دخول دائمة). تاليًا: التفويض — تقرير ما يُسمح للمستخدم المُصادَق بفعله.