رموز التحديث
رموز التحديث
في الدروس السابقة بنيت نظامًا يُصدر رمز وصول JWT عند تسجيل الدخول ويتحقق منه في كل طلب محمي. يعمل ذلك جيدًا، لكنه يحمل مقايضةً خفيةً: كي يكون الرمز مفيدًا دون الرجوع إلى قاعدة البيانات مع كل طلب، يجب أن يكون عديم الحالة (stateless)، ما يعني أن الخادم لا يستطيع إبطاله قبل انتهاء صلاحيته. الرموز قصيرة العمر (5–15 دقيقة) تُقلّص نافذة الإبطال، لكنها تعني أيضًا أن المستخدم يجب أن يُعيد تسجيل الدخول كل بضع دقائق — وهو أمر يُفسد تجربة المستخدم.
تحلّ رموز التحديث (Refresh Tokens) هذا التوتر. الفكرة بسيطة:
- أصدر رمز وصول قصير العمر (5–15 دقيقة) — هو بيانات الاعتماد الحاملة لاستدعاءات API.
- أصدر رمز تحديث طويل العمر (أيام إلى أسابيع) — يُخزَّن بأمان ولا يُستخدم إلا للحصول على رموز وصول جديدة.
- عند انتهاء صلاحية رمز الوصول، يُقدّم العميل رمزَ التحديث إلى نقطة نهاية مخصصة
/auth/refreshويحصل على رمز وصول جديد دون مطالبة المستخدم ببيانات الاعتماد مجددًا.
خطوات تدفق التحديث
- يُسجّل المستخدم دخوله. يُعيد الخادم
{ accessToken, refreshToken }. يُحفظ رمز التحديث في قاعدة البيانات ويُرسَل إلى العميل. - يُخزّن العميل رمز الوصول في الذاكرة (لا في
localStorageللتطبيقات عالية الأمان) ورمز التحديث في ملف تعريف ارتباطHttpOnlyأو تخزين آمن. - يُرسل العميل الطلبات مع رمز الوصول في الرأسية
Authorization: Bearer <token>. - عند إعادة الخادم للاستجابة
401 Unauthorized(انتهت صلاحية رمز الوصول)، يستدعي العميلPOST /auth/refreshمع رمز التحديث. - يتحقق الخادم من رمز التحديث مقابل قاعدة البيانات (يتحقق من وجوده وعدم إبطاله وعدم انتهاء صلاحيته). إن كان صالحًا، يُصدر رمز وصول جديدًا (ويُدوّر رمز التحديث اختياريًا).
- يُعيد العميل إرسال الطلب الأصلي مع رمز الوصول الجديد.
تخزين رموز التحديث
على عكس رموز الوصول، تمتلك رموز التحديث حالة (stateful). يجب تخزينها في قاعدة البيانات حتى يمكن إبطالها. يبدو الكيان البسيط كالتالي:
UUID.randomUUID().toString() أو بايتات SecureRandom مُشفَّرة بـ hex) يتيح حذفه أو وضع علامة مُبطَلة عليه في قاعدة البيانات على الفور.
خدمة RefreshTokenService
متحكم المصادقة — نقطتا الدخول والتحديث
تدوير رمز التحديث واكتشاف إعادة الاستخدام
لاحظ استدعاء createFor(user) داخل نقطة نهاية /auth/refresh. هذا هو تدوير رمز التحديث (refresh token rotation): كل تحديث ناجح يُصدر رمزَ تحديث جديدًا ويُبطل القديم. وهو أسلوب أمني بالغ الأهمية.
لماذا يهم التدوير؟ إن سرق مهاجم رمز تحديث واستخدمه، فإن التدوير يعني أن رمز المستخدم الشرعي يُبطَل في نفس اللحظة. في المرة التالية التي يحاول فيها المستخدم الشرعي التحديث، ستفشل عملية التحقق — مُنبّهةً إياك (وربما إياه) بأن الرمز قد تعرض للاختراق. بدون تدوير، يظل رمز التحديث المسروق صالحًا حتى تاريخ انتهاء صلاحيته الطبيعي.
توصيات عمر رمز الوصول
هذه نقاط بداية عملية — اضبطها وفقًا لنموذج التهديد لديك:
- رمز الوصول: 5–15 دقيقة. عديم الحالة، يُتحقق منه في الذاكرة. النافذة القصيرة تُقلّل تعرض الرمز المسروق للخطر.
- رمز التحديث: 7–30 يومًا. يمتلك حالة، مدعوم بقاعدة بيانات، يُدار عند الاستخدام. عمود
expiresAtفي قاعدة البيانات هو الموعد النهائي الصارم. - حد الجلسة المطلق: حدد مدة جلسة قصوى إجمالية (مثلًا 90 يومًا). بعدها يجب على المستخدم إعادة المصادقة بغض النظر عن نشاط الرمز. يمنع هذا جلسة تسجيل دخول واحدة من الاستمرار إلى الأبد على جهاز مشترك.
تخزين الرموز بأمان على العميل
تصميم رمز الخادم لا يهم إلا إذا خزّن العميل الرموز بشكل صحيح:
- رمز الوصول: احتفظ به في ذاكرة JavaScript (متغير على مستوى الوحدة أو مخزن الحالة). لا في
localStorageأوsessionStorage— يستطيع هجوم XSS قراءتهما. - رمز التحديث: أرسله في ملف تعريف ارتباط بخاصيات
HttpOnly; Secure; SameSite=Strict. تجعلهHttpOnlyغير مرئي لـ JavaScript؛ وتمنعSameSite=Strictهجمات CSRF. في هذا المتغير الأكثر صرامة، تقرأ نقطة نهاية الخادم/auth/refreshالرمز من ملف تعريف الارتباط بدلًا من جسم الطلب.
الخلاصة
رموز التحديث هي الآلية التي تتيح لك الحصول على رموز وصول قصيرة العمر يصعب إساءة استخدامها وتجربة مستخدم سلسة في آنٍ واحد. تظل رموز الوصول عديمة الحالة وسريعة؛ أما رموز التحديث فتمتلك حالة وقابلة للإبطال. اقرنها بالتدوير للكشف عن السرقة، وخزّنها في قاعدة بيانات من جهة الخادم، واجمعها مع ملفات تعريف الارتباط HttpOnly من جهة العميل لتحقيق أفضل وضع أمني. في الدرس القادم ستتعرف على OAuth2 وOpenID Connect — البروتوكولات المعيارية في الصناعة التي تُضفرم بالضبط هذا النوع من دورة حياة الرمز.