رموز JSON المميزة (JWT) شرح مفصّل
رموز JSON المميزة (JWT) شرح مفصّل
في الدرس السابق رأيتَ لماذا تُعدّ المصادقة عديمة الحالة النموذجَ الصحيح لواجهات REST. يُركّز هذا الدرس على تنسيق الرمز المميز الذي يُتيح ذلك: JSON Web Token. بنهاية الدرس ستتمكّن من قراءة أي JWT بعينيك مباشرةً، ومعرفة ما ينتمي إليه بالضبط ولماذا، والتعرّف على الآثار الأمنية لكل خيار تصميمي.
شكل رمز JWT
الـ JWT سلسلة نصية مدمجة وآمنة للاستخدام في عناوين URL، مقسّمة إلى ثلاثة أقسام مشفَّرة بـ Base64URL مفصولة بنقاط:
الأجزاء الثلاثة هي: الترويسة (Header) · الحمولة (Payload) · التوقيع (Signature). إذا أزلتَ تشفير Base64URL ستحصل على كائنَي JSON وكتلة ثنائية.
الجزء الأول — الترويسة
الترويسة كائن JSON صغير يصف الرمز نفسه — لا المستخدم ولا الصلاحيات، بل تنسيق الرمز فحسب.
الحقلان الإلزاميان هما:
alg— الخوارزمية التشفيرية المستخدمة لإنشاء التوقيع. القيم الشائعة:HS256(HMAC-SHA-256، خوارزمية متماثلة تستخدم سرًّا مشتركًا) وRS256(RSA-SHA-256، خوارزمية غير متماثلة تستخدم زوج مفاتيح خاص/عام).typ— نوع الرمز. قيمته دائمًا"JWT"لرموز JSON Web Token.
alg: none. ثغرة أمنية شهيرة في مكتبات JWT المبكّرة سمحت للمهاجمين بضبط "alg": "none"، ممّا جعل المكتبة تتخطّى التحقق من التوقيع كليًّا. اضبط مكتبتك دائمًا لقبول الخوارزميات المحدّدة التي يستخدمها تطبيقك فقط — ولا تسمح بـ none في بيئة الإنتاج أبدًا.
الجزء الثاني — الحمولة (المطالبات)
الحمولة هي قلب الرمز. إنها كائن JSON يحتوي على مطالبات (claims) — تصريحات تتعلق بالموضوع (عادةً المستخدم المسجّل دخوله) وبيانات وصفية حول الرمز نفسه. تنقسم المطالبات إلى ثلاث فئات.
المطالبات المسجَّلة
أسماء موحَّدة معرَّفة في RFC 7519. استخدامها باتساق يُتيح التشغيل البيني بين الأنظمة المختلفة. أهمّها في تطبيق Spring Security:
sub(الموضوع) — معرّف فريد للمبدأ الذي يمثّله الرمز، عادةً معرّف مستخدم أو اسم مستخدم.iss(المصدر) — من أصدر الرمز، مثل"https://api.myapp.com". ينبغي لخوادم الموارد رفض الرموز الصادرة من مصادر غير متوقّعة.aud(الجمهور) — المستلم أو المستلمون المقصودون للرمز. ينبغي رفض رمز مُعدّ لـ"mobile-app"من قِبل خدمة"admin-dashboard".iat(وقت الإصدار) — طابع زمني Unix يسجّل وقت إنشاء الرمز.exp(وقت انتهاء الصلاحية) — طابع زمني Unix يجب بعده رفض الرمز. هذا خطّ دفاعك الأول: الرموز قصيرة العمر تُقلّل بشكل كبير من الضرر في حالة سرقة الرمز.nbf(ليس قبل) — طابع زمني Unix يجب قبله رفض الرمز. نادر الاستخدام لكن يُحتاج إليه أحيانًا في سيناريوهات التفعيل المؤجَّل.jti(معرّف JWT) — معرّف فريد للرمز. تخزين قيمjtiوالتحقق منها يُتيح تنفيذ إلغاء صلاحية الرمز دون الحاجة إلى مخزن جلسات كامل.
المطالبات العامة
مطالبات خاصة بالتطبيق تُسجَّل في سجل مطالبات IANA JWT لتجنّب التعارضات، أو تُعطى نطاقًا بـ URI: "https://myapp.com/roles". عمليًّا، تتخطّى معظم الفِرق التسجيل الرسمي في الخدمات الداخلية.
المطالبات الخاصة
مطالبات متّفق عليها بين المنتج والمستهلك — غير مسجَّلة في أيّ مكان. أمثلة نموذجية: roles، tenantId، plan. هذه هي المطالبات التي ستقرأها فلترة Spring Security لبناء كائن Authentication.
حمولة واقعية لواجهة برمجية متعددة المستأجرين (SaaS):
الجزء الثالث — التوقيع
يربط التوقيع الترويسة والحمولة معًا ويُثبت أنّهما صدرا من طرف يمتلك المفتاح الصحيح. يُحسَب على الترويسة والحمولة المشفَّرتين:
بالنسبة لـ RS256 العملية هي نفسها غير أن المُوقّع يستخدم مفتاحه الخاص والمتحقّقون يستخدمون المفتاح العام المقابل. هذا التباين غير المتماثل محوري في أمان الخدمات المصغَّرة:
- خدمة المصادقة وحدها تعرف المفتاح الخاص — إذًا هي وحدها تستطيع إصدار الرموز.
- كل خدمة مصغَّرة مصبَّ عليها تمتلك المفتاح العام فقط — يمكنها التحقق من الرموز لكن لا يمكنها تزويرها.
- اختراق خدمة مصب لا يعرّض مفتاح توقيع خدمة المصادقة للخطر.
تجميع الأجزاء — الترميز وفك الترميز يدويًّا
فهم عملية الترميز يُزيل الغموض. إليك ما يحدث عند إنشاء رمز والتحقق منه، معبَّرًا عنه بشبه كود بمصطلحات Java:
MessageDigest.isEqual() لسبب وجيه. المقارنة الساذجة بـ Arrays.equals() تتوقف عند أول عدم تطابق، مما يُسرّب معلومات توقيت يمكن للمهاجم استغلالها لتزوير التوقيعات بايت بايت. استخدم دائمًا مقارنة بوقت ثابت عند مقارنة القيم التشفيرية.
مقايضات التصميم الأساسية التي يجب على كل مطوّر معرفتها
قبل إضافة مطالبة أو تعديل أوقات انتهاء الصلاحية، افهم هذه المقايضات:
- حجم الرمز مقابل ثراء المطالبات. كل مطالبة تُضيفها تُكبّر الرمز الذي يُرسَل كترويسة في كل طلب HTTP. أبقِ الحمولات خفيفة — المعرّفات والأدوار مناسبة؛ كائنات الملف الشخصي الكاملة ليست كذلك.
- انتهاء الصلاحية مقابل الإلغاء. الـ JWT عديمة الحالة، لذا لا يوجد إلغاء مدمج. بمجرد الإصدار يظل الرمز صالحًا حتى انتهاء صلاحيته. انتهاء الصلاحية خلال ساعة يعني أن رمزًا مسروقًا يمكن استخدامه لمدة ساعة. عشر دقائق تُقلّل تلك النافزة لكن تستلزم المزيد من رحلات تحديث الرمز. وازن بين تجربة المستخدم والمخاطر.
- الإلغاء القائم على JTI. إذا احتجتَ إلى إلغاء فوري (تسجيل خروج، استجابة لاختراق)، خزّن قائمة حجب لقيم
jtiفي Redis أو قاعدة بيانات، مع التحقق منها في كل طلب. هذا يُعيد الحالة لكن فقط للرموز المحجوبة — المسار السعيد يبقى عديم الحالة. - انحراف الساعة. الخوادم في النظام الموزَّع نادرًا ما تكون ساعاتها متزامنة تمامًا. مكتبات مثل JJWT وNimbus تُتيح ضبط انحراف ساعة مسموح به (عادةً 60 ثانية) حتى يُقبل رمز بـ
exp = Tبعد ثوانٍ قليلة منT.
الخلاصة
الـ JWT ثلاثة أقسام مشفَّرة بـ Base64URL: ترويسة تصف الخوارزمية، وحمولة تحتوي على المطالبات المسجَّلة والمخصَّصة، وتوقيع يربط الجزأين الآخرين تشفيريًّا. الحمولة قابلة للقراءة من أيّ شخص لكنها محمية من العبث. استخدم HS256 للأنظمة ذات الخدمة الواحدة وRS256/ES256 للخدمات المصغَّرة. أبقِ الحمولات خفيفة، واضبط أوقات انتهاء صلاحية قصيرة، ولا تضع أبدًا بيانات حساسة في رمز. مع هذا الأساس في مكانه، يُريك الدرس التالي كيفية توليد رموز JWT والتحقق منها في تطبيق Spring Boot 3 حقيقي باستخدام مكتبة JJWT.