أرغب في تطبيق المصادقة المستندة إلى JWT على واجهة برمجة التطبيقات REST الجديدة لدينا. ولكن بما أن انتهاء الصلاحية قد تم تعيينه في الرمز المميز ، فهل من الممكن إطالة مدة صلاحيته تلقائيًا؟ لا أريد أن يحتاج المستخدمون إلى تسجيل الدخول بعد كل X دقيقة إذا كانوا يستخدمون التطبيق بنشاط في تلك الفترة. سيكون ذلك فشل UX ضخم.
لكن إطالة مدة الصلاحية يخلق رمزًا جديدًا (ولا يزال الرمز القديم صالحًا حتى انتهاء صلاحيته). وتوليد رمز جديد بعد كل طلب يبدو سخيفا بالنسبة لي. يبدو وكأنه مشكلة أمنية عندما يكون هناك أكثر من رمز مميز صالح في نفس الوقت. بالطبع يمكنني إبطال المستخدم القديم باستخدام قائمة سوداء ولكني سأحتاج إلى تخزين الرموز. واحدة من فوائد JWT هو عدم التخزين.
لقد وجدت كيف Auth0 حلها. لا يستخدمون رمز JWT فحسب ، بل يستخدمون الرمز المميز للتحديث: https://docs.auth0.com/refresh-token
لكن مرة أخرى ، لتنفيذ هذا (بدون Auth0) ، سأحتاج إلى تخزين الرموز المميزة للتحديث والحفاظ على صلاحيتها. ما هي الفائدة الحقيقية إذن؟ لماذا لا يوجد لديك سوى رمز واحد (وليس JWT) والحفاظ على انتهاء الصلاحية على الخادم؟
هل هناك خيارات أخرى؟ هل استخدام JWT غير مناسب لهذا السيناريو؟
أنا أعمل في Auth0 وشاركت في تصميم ميزة الرمز المميز للتحديث.
كل هذا يتوقف على نوع التطبيق وهنا هو نهجنا الموصى به.
هناك نمط جيد يتمثل في تحديث الرمز المميز قبل انتهاء صلاحيته.
قم بتعيين انتهاء صلاحية الرمز المميز على أسبوع واحد وتحديث الرمز المميز في كل مرة يفتح المستخدم فيها تطبيق الويب وكل ساعة واحدة. إذا لم يفتح المستخدم التطبيق لأكثر من أسبوع ، فسيتعين عليه تسجيل الدخول مرة أخرى وهذا تطبيق ويب مقبول UX.
لتحديث الرمز المميز ، تحتاج واجهة برمجة التطبيقات (API) الخاصة بك إلى نقطة نهاية جديدة تستقبل JWT صالحة وليس منتهية الصلاحية وترجع نفس JWT الموقعة مع حقل انتهاء الصلاحية الجديد. ثم سيقوم تطبيق الويب بتخزين الرمز في مكان ما.
معظم التطبيقات الأصلية تقوم بتسجيل الدخول مرة واحدة فقط.
الفكرة هي أن الرمز المميز للتحديث لا ينتهي أبدًا ، ويمكن استبداله دائمًا باستخدام JWT صالح.
إن مشكلة الرمز المميز التي لا تنتهي أبدًا هي أبدًا لا تعني أبدًا. ماذا تفعل إذا فقدت هاتفك؟ لذلك ، يجب أن يكون المستخدم قابلاً للتعريف بطريقة أو بأخرى ويجب أن يوفر التطبيق طريقة لإلغاء الوصول. قررنا استخدام اسم الجهاز ، على سبيل المثال "باد ماريو". ثم يمكن للمستخدم الانتقال إلى التطبيق وإلغاء الوصول إلى "maryo iPad".
هناك طريقة أخرى لإلغاء رمز التحديث في أحداث معينة. حدث مثير للاهتمام هو تغيير كلمة المرور.
نعتقد أن JWT ليست مفيدة لحالات الاستخدام هذه ، لذا نستخدم سلسلة عشوائية تم إنشاؤها ونخزنها في صفنا.
في حالة تعاملك مع المصادقة بنفسك (أي عدم استخدام مزود مثل Auth0) ، قد يعمل ما يلي:
سيتم تعيين علامة "reauth" في الواجهة الخلفية لقاعدة البيانات عندما يقوم المستخدم ، على سبيل المثال ، بإعادة تعيين كلمة المرور الخاصة به. يتم إزالة العلم عندما يسجل المستخدم في المرة القادمة.
بالإضافة إلى ذلك ، دعنا نقول أن لديك سياسة حيث يجب على المستخدم تسجيل الدخول مرة واحدة على الأقل كل 72 ساعة. في هذه الحالة ، يقوم منطق تحديث الرمز المميز لواجهة برمجة التطبيقات أيضًا بفحص تاريخ آخر تسجيل دخول للمستخدم من قاعدة بيانات المستخدم ورفض/السماح بتحديث الرمز المميز على هذا الأساس.
الحل البديل لإبطال JWTs ، دون أي تخزين آمن إضافي على الواجهة الخلفية ، هو تطبيق عمود عدد صحيح jwt_version
جديد على جدول المستخدمين. إذا كان المستخدم يرغب في تسجيل الخروج أو انتهاء صلاحية الرموز الموجودة ، فإنهم ببساطة يقومون بزيادة حقل jwt_version
.
عند إنشاء JWT جديد ، قم بترميز jwt_version
في حمولة JWT ، مع زيادة القيمة اختياريًا إذا كان يجب على JWT الجديدة استبدال كل الآخرين.
عند التحقق من صحة JWT ، تتم مقارنة حقل jwt_version
بجانب user_id
ويتم منح التفويض فقط إذا كان مطابقًا.
كنت العبث عند نقل تطبيقاتنا إلى HTML5 مع RESTful apis في الواجهة الخلفية. الحل الذي توصلت إليه هو:
كما ترون ، هذا يقلل من طلبات الرمز المميز للتحديث المتكرر. إذا أغلق المستخدم المتصفح/التطبيق قبل تشغيل استدعاء الرمز المميز ، فسوف تنتهي صلاحية الرمز المميز السابق في الوقت المناسب وسيتعين على المستخدم إعادة تسجيل الدخول.
يمكن تنفيذ إستراتيجية أكثر تعقيدًا لتلبية خمول المستخدم (مثل إهمال علامة تبويب المتصفح المفتوحة). في هذه الحالة ، يجب أن تتضمن استدعاء الرمز المميز للتجديد وقت انتهاء الصلاحية المتوقع والذي يجب ألا يتجاوز وقت الجلسة المحدد. سيتعين على التطبيق تتبع آخر تفاعل للمستخدم وفقًا لذلك.
لا أحب فكرة تعيين مدة صلاحية طويلة ، وبالتالي قد لا تعمل هذه الطريقة بشكل جيد مع التطبيقات الأصلية التي تتطلب مصادقة أقل تكرارًا.
سؤال جيد - وهناك ثروة من المعلومات في السؤال نفسه.
المقالة تحديث الرموز: متى تستخدمهم وكيف يتفاعلون مع JWTs يعطي فكرة جيدة عن هذا السيناريو. بعض النقاط هي: -
يمكنك أيضًا إلقاء نظرة على auth0/angular-jwt angularjs
لواجهة برمجة تطبيقات الويب. قراءة تمكين رموز تحديث OAuth في تطبيق AngularJS باستخدام ASP .NET Web API 2 و Owin
إذا كنت تستخدم العقدة (React/Redux/Universal JS) ، فيمكنك تثبيت npm i -S jwt-autorefresh
.
تقوم هذه المكتبة بجدولة تحديث الرموز المميزة لـ JWT على عدد مستخدم محسوب من الثواني قبل انتهاء صلاحية رمز الوصول (استنادًا إلى مطالبة exp المشفرة في الرمز المميز). إنه يحتوي على مجموعة اختبار شاملة ويفحص عددًا قليلاً من الشروط لضمان أن أي نشاط غريب مصحوب برسالة وصفية بخصوص التكوينات الخاطئة من بيئتك.
تنفيذ المثال الكامل
import autorefresh from 'jwt-autorefresh'
/** Events in your app that are triggered when your user becomes authorized or deauthorized. */
import { onAuthorize, onDeauthorize } from './events'
/** Your refresh token mechanism, returning a promise that resolves to the new access tokenFunction (library does not care about your method of persisting tokens) */
const refresh = () => {
const init = { method: 'POST'
, headers: { 'Content-Type': `application/x-www-form-urlencoded` }
, body: `refresh_token=${localStorage.refresh_token}&grant_type=refresh_token`
}
return fetch('/oauth/token', init)
.then(res => res.json())
.then(({ token_type, access_token, expires_in, refresh_token }) => {
localStorage.access_token = access_token
localStorage.refresh_token = refresh_token
return access_token
})
}
/** You supply a leadSeconds number or function that generates a number of seconds that the refresh should occur prior to the access token expiring */
const leadSeconds = () => {
/** Generate random additional seconds (up to 30 in this case) to append to the lead time to ensure multiple clients dont schedule simultaneous refresh */
const jitter = Math.floor(Math.random() * 30)
/** Schedule autorefresh to occur 60 to 90 seconds prior to token expiration */
return 60 + jitter
}
let start = autorefresh({ refresh, leadSeconds })
let cancel = () => {}
onAuthorize(access_token => {
cancel()
cancel = start(access_token)
})
onDeauthorize(() => cancel())
إخلاء المسئولية: أنا المشرف
لقد قمت بالفعل بتنفيذ هذا في PHP باستخدام عميل Guzzle لإنشاء مكتبة عميل لـ api ، لكن يجب أن يعمل المفهوم لأنظمة تشغيل أخرى.
في الأساس ، أنا أصدر رمزين ، واحد (5 دقائق) قصير وواحد ينتهي بعد أسبوع. تستخدم مكتبة العميل الوسيطة لمحاولة تحديث واحد من الرمز المميز إذا كان يتلقى استجابة 401 لبعض الطلب. سيحاول بعد ذلك الطلب الأصلي مرة أخرى ، وإذا كان قادراً على تحديث يحصل على الاستجابة الصحيحة ، بشفافية للمستخدم. إذا فشلت ، فسترسل فقط 401 إلى المستخدم.
في حالة انتهاء صلاحية الرمز المميز القصير ، ولكن لا يزال الحجية والرمز المميز الطويل صالحًا وأصليًا ، فسوف يتم تحديث الرمز المميز القصير باستخدام نقطة نهاية خاصة على الخدمة التي يصادق عليها الرمز المميز (هذا هو الشيء الوحيد الذي يمكن استخدامه من أجله). سيستخدم بعد ذلك الرمز المميز للحصول على رمز طويل جديد ، وبالتالي تمديده أسبوعًا آخر في كل مرة يقوم فيها بتحديث الرمز المميز.
يسمح لنا هذا النهج أيضًا بإلغاء الوصول خلال 5 دقائق على الأكثر ، وهو أمر مقبول لاستخدامنا دون الحاجة إلى تخزين قائمة سوداء من الرموز.
التحرير المتأخر: إعادة قراءة هذا الشهر بعد أن كان جديدًا في رأسي ، ينبغي أن أشير إلى أنه يمكنك إلغاء الوصول عند تحديث الرمز المميز لأنه يتيح فرصة لإجراء مكالمات أكثر تكلفة (مثل الاتصال بقاعدة البيانات لمعرفة ما إذا كان المستخدم تم حظره) دون الدفع مقابل كل مكالمة واحدة لخدمتك.
ماذا عن هذا النهج:
لا نطلب نقطة نهاية إضافية لتحديث الرمز المميز في هذه الحالة. سوف نقدر أي feedack.
لقد قمت بحل هذه المشكلة عن طريق إضافة متغير في البيانات الرمزية:
softexp - I set this to 5 mins (300 seconds)
قمت بتعيين خيار expiresIn
على وقتي المطلوب قبل أن يضطر المستخدم إلى تسجيل الدخول مرة أخرى. يتم تعيين الألغام إلى 30 دقيقة. يجب أن يكون هذا أكبر من قيمة softexp
.
عندما يرسل تطبيق العميل الخاص بي طلبًا إلى واجهة برمجة تطبيقات الخادم (حيث يلزم الرمز المميز ، على سبيل المثال ، صفحة قائمة العملاء) ، يتحقق الخادم مما إذا كان الرمز المميز المقدم لا يزال صالحًا أم لا بناءً على قيمة انتهاء الصلاحية الأصلية (expiresIn
). إذا لم تكن صالحة ، فسيستجيب الخادم بحالة معينة لهذا الخطأ ، على سبيل المثال. INVALID_TOKEN
.
إذا كان الرمز المميز لا يزال صالحًا استنادًا إلى القيمة expiredIn
، ولكنه تجاوز بالفعل القيمة softexp
، فسيستجيب الخادم بحالة منفصلة لهذا الخطأ ، على سبيل المثال. EXPIRED_TOKEN
:
(Math.floor(Date.now() / 1000) > decoded.softexp)
من جانب العميل ، إذا تلقى استجابة EXPIRED_TOKEN
، فيجب عليه تجديد الرمز تلقائيًا عن طريق إرسال طلب تجديد إلى الخادم. هذا شفاف للمستخدم ويتم العناية به تلقائيًا لتطبيق العميل.
يجب أن تتحقق طريقة التجديد في الخادم مما إذا كان الرمز المميز لا يزال صالحًا:
jwt.verify(token, secret, (err, decoded) => {})
سيرفض الخادم تجديد الرموز إذا فشل في الطريقة المذكورة أعلاه.
فيما يلي الخطوات اللازمة لإلغاء رمز الدخول إلى JWT:
1) عند تسجيل الدخول ، أرسل رمزين (رمز الوصول ، رمز التحديث) استجابةً للعميل.
2) سيكون لرمز الوصول وقت انتهاء صلاحية أقل ، وسوف يكون للتحديث وقت انتهاء صلاحية طويل.
3) سيقوم العميل (الواجهة الأمامية) بتخزين رمز التحديث في وحدة التخزين المحلية الخاصة به والوصول إلى الرمز المميز في ملفات تعريف الارتباط.
4) سيستخدم العميل رمز الوصول للاتصال بـ apis. ولكن عندما تنتهي صلاحيته ، اختر رمز التحديث من وحدة التخزين المحلية واتصل بـ aph server api للحصول على الرمز المميز الجديد.
5) سيكون لخادم المصادقة الخاص بك واجهة برمجة تطبيقات معروضة والتي تقبل رمز التحديث وتحقق من صلاحيته وتعيد رمز وصول جديد.
6) بمجرد انتهاء صلاحية الرمز المميز ، سيتم تسجيل خروج المستخدم.
واسمحوا لي أن أعرف إذا كنت بحاجة إلى مزيد من التفاصيل ، يمكنني مشاركة الكود (Java + Spring boot) أيضًا.