رد فعل المصادقة


مشكلة


يعد التخويل أحد المشكلات الأولى التي يواجهها المطورون عند بدء مشروع جديد. وأحد أكثر أنواع التخويل شيوعًا (من تجربتي) هو التخويل المستند إلى الرمز (عادةً ما يستخدم JWT).


من وجهة نظري ، يبدو هذا المقال "ما أردت أن أقرأه قبل أسبوعين". كان هدفي هو كتابة رمز بسيط وقابل لإعادة الاستخدام مع واجهة نظيفة ومباشرة. كان لدي المتطلبات التالية لتنفيذ عملي لإدارة المصادقة:


  • يجب أن يتم تخزين الرموز في التخزين المحلي
  • يجب استعادة الرموز عند إعادة تحميل الصفحة
  • يجب أن يتم تمرير رمز الوصول في طلبات الشبكة
  • بعد انتهاء صلاحية الرمز المميز ، يجب تحديث الرمز المميز للتحديث في حالة تقديم الرمز الأخير
  • يجب أن تتمتع مكونات React بالوصول إلى معلومات المصادقة لتقديم واجهة المستخدم المناسبة
  • يجب أن يكون الحل مع React النقي (بدون Redux ، thunk ، إلخ.)

بالنسبة لي كان أحد أكثر الأسئلة تحديا:


  • كيفية الاحتفاظ بمزامنة حالة مكونات React وبيانات التخزين المحلية؟
  • كيفية الحصول على الرمز المميز داخل الجلب دون تمريره عبر شجرة العناصر بأكملها (خاصةً إذا كنا نريد استخدام هذا الجلب في إجراءات thunk لاحقًا على سبيل المثال)

ولكن دعنا نحل المشكلات خطوة بخطوة. أولاً ، سنقوم بإنشاء token provider لتخزين الرموز وتوفير إمكانية الاستماع إلى التغييرات. بعد ذلك ، سنقوم بإنشاء auth provider ، والتفاف فعلي حول token provider لإنشاء روابط لمكونات React ، وجلب المنشطات وبعض الطرق الإضافية. وفي النهاية ، سننظر في كيفية استخدام هذا الحل في المشروع.


أريد فقط npm install ... الإنتاج


جمعت بالفعل الحزمة التي تحتوي على كل ما هو موضح أدناه (وأكثر قليلاً). تحتاج فقط إلى تثبيته بواسطة الأمر:


 npm install react-token-auth 

واتبع الأمثلة في مستودع GitHub للرد التفاعلي.


حل


قبل حل المشكلة ، سأفترض أن لدينا واجهة خلفية تقوم بإرجاع كائن له حق الوصول وتحديث الرموز. كل رمز مميز له تنسيق JWT . قد يبدو مثل هذا الكائن:


 { "accessToken": "...", "refreshToken": "..." } 

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


JWT


إذا كنت لا تعرف ما هو رمز JWT ، فإن الخيار الأفضل هو الانتقال إلى jwt.io وإلقاء نظرة على كيفية عمله. من المهم الآن أن يحتوي الرمز المميز لـ JWT على معلومات مشفرة (بتنسيق Base64 ) حول المستخدم تتيح له المصادقة على الخادم.


عادةً ما يشتمل الرمز المميز JWT على 3 أجزاء مقسومة على النقاط ويبدو أنه:


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.yOZC0rjfSopcpJ-d3BWE8-BkoLR_SCqPdJpq8Wn-1Mc


إذا قمنا بفك تشفير الجزء الأوسط ( eyJu...Mn0 ) من هذا الرمز المميز ، eyJu...Mn0 على JSON التالي:


 { "name": "John Doe", "iat": 1516239022, "exp": 1516239022 } 

باستخدام هذه المعلومات ، سنتمكن من الحصول على تاريخ انتهاء صلاحية الرمز المميز.


مزود الرمز


كما ذكرت من قبل ، فإن خطوتنا الأولى هي إنشاء موفر الرمز المميز. سيعمل موفر الرمز المميز مباشرة مع التخزين المحلي وجميع تغييرات الرمز المميز التي سنفعلها من خلاله. سيتيح لنا الاستماع إلى التغييرات من أي مكان وإخطار المستمعين على الفور بالتغييرات (ولكن بعد ذلك بقليل). سيكون لدى واجهة الموفر الطرق التالية:


  • getToken() للحصول على الرمز المميز الحالي (سيتم استخدامه في جلب)
  • setToken() لضبط الرمز المميز بعد تسجيل الدخول أو تسجيل الخروج أو التسجيل
  • isLoggedIn() للتحقق هو تسجيل دخول المستخدم
  • subscribe() لإعطاء الموفر وظيفة يجب استدعاؤها بعد أي تغيير رمزي
  • unsubscribe() لإزالة المشترك

تقوم دالة createTokenProvider() بإنشاء مثيل لموفر الرمز المميز مع الواجهة الموصوفة:


 const createTokenProvider = () => { /* Implementation */ return { getToken, isLoggedIn, setToken, subscribe, unsubscribe, }; }; 

يجب أن تكون جميع التعليمات البرمجية التالية داخل الدالة createTokenProvider.


لنبدأ بإنشاء متغير لتخزين الرموز واستعادة البيانات من التخزين المحلي (للتأكد من أن الجلسة لن تضيع بعد إعادة تحميل الصفحة):


 let _token: { accessToken: string, refreshToken: string } = JSON.parse(localStorage.getItem('REACT_TOKEN_AUTH') || '') || null; 

نحن الآن بحاجة إلى إنشاء بعض الوظائف الإضافية للعمل مع الرموز JWT. في الوقت الحالي ، يبدو الرمز المميز لـ JWT بمثابة سلسلة سحرية ، لكن ليس من المهم تحليلها ومحاولة استخراج تاريخ انتهاء الصلاحية. getExpirationDate() الدالة getExpirationDate() رمزًا مميزًا لـ JWT كمعلمة وإرجاع الطابع الزمني لتاريخ انتهاء الصلاحية عند النجاح (أو null عند الفشل):


 const getExpirationDate = (jwtToken?: string): number | null => { if (!jwtToken) { return null; } const jwt = JSON.parse(atob(jwtToken.split('.')[1])); // multiply by 1000 to convert seconds into milliseconds return jwt && jwt.exp && jwt.exp * 1000 || null; }; 

وواحد أكثر استخدام الدالة isExpired() للتحقق هو انتهت صلاحية الطابع الزمني. تُرجع هذه الدالة true إذا كان الطابع الزمني لانتهاء الصلاحية Date.now() وإذا كان أقل من Date.now() .


 const isExpired = (exp?: number) => { if (!exp) { return false; } return Date.now() > exp; }; 

الوقت لإنشاء أول وظيفة لواجهة موفر الرمز المميز. يجب أن ترجع الدالة getToken() الرمز المميز وتحديثه إذا لزم الأمر. يجب أن تكون هذه الوظيفة غير async لأنها قد تقدم طلب شبكة لتحديث الرمز المميز.


باستخدام دالات سابقة تم إنشاؤها ، يمكننا التحقق من أن رموز الوصول منتهية الصلاحية أم لا ( isExpired(getExpirationDate(_token.accessToken)) ). وفي الحالة الأولى لتقديم طلب لتحديث الرمز المميز. بعد ذلك ، يمكننا حفظ الرموز المميزة (مع الدالة setToken() لم يتم تنفيذها بعد). وأخيراً ، يمكننا إرجاع رمز الوصول:


 const getToken = async () => { if (!_token) { return null; } if (isExpired(getExpirationDate(_token.accessToken))) { const updatedToken = await fetch('/update-token', { method: 'POST', body: _token.refreshToken }) .then(r => r.json()); setToken(updatedToken); } return _token && _token.accessToken; }; 

isLoggedIn() الوظيفة isLoggedIn() بسيطة: ستعود بشكل صحيح إذا لم يكن _tokens null ولن نتحقق من انتهاء صلاحية الرمز المميز للوصول (في هذه الحالة ، لن نعرف عن رمز وصول صلاحية انتهاء الصلاحية حتى نفشل في الحصول على الرمز المميز ، ولكن عادةً ما يكون ذلك كافيًا ، ودعنا نحافظ على وظيفة isLoggedIn متزامن):


 const isLoggedIn = () => { return !!_token; }; 

أعتقد أنه وقت مناسب لإنشاء وظائف لإدارة المراقبين. سوف ننفذ شيئًا مماثلاً لنموذج Observer ، وقبل كل شيء ، سننشئ مجموعة لتخزين جميع المراقبين لدينا. نتوقع أن يكون كل عنصر في هذه المجموعة هو الوظيفة التي يجب أن نسميها بعد كل تغيير للرموز:


 let observers: Array<(isLogged: boolean) => void> = []; 

الآن يمكننا إنشاء طرق subscribe() unsubscribe() . أول واحد سيضيف مراقبًا جديدًا إلى المصفوفة التي تم إنشاؤها سابقًا قليلاً ، والثاني سيزيل مراقبًا من القائمة.


 const subscribe = (observer: (isLogged: boolean) => void) => { observers.push(observer); }; const unsubscribe = (observer: (isLogged: boolean) => void) => { observers = observers.filter(_observer => _observer !== observer); }; 

يمكنك بالفعل أن ترى من واجهة وظائف subscribe() unsubscribe() أننا سنرسل للمراقبين فقط حقيقة أن المستخدم قام بتسجيل الدخول. ولكن بشكل عام ، يمكنك إرسال كل ما تريده (الرمز المميز بالكامل ، وقت انتهاء الصلاحية ، إلخ ...). ولكن لأغراضنا ، يكفي إرسال علم منطقي.


دعونا ننشئ وظيفة استخدام صغيرة notify() ستأخذ هذه العلامة وترسلها إلى جميع المراقبين:


 const notify = () => { const isLogged = isLoggedIn(); observers.forEach(observer => observer(isLogged)); }; 

وأخيرًا وليس آخرًا ، ما نحتاج إلى تطبيقه هو setToken() . الغرض من هذه الوظيفة هو حفظ الرموز المميزة في التخزين المحلي (أو تنظيف التخزين المحلي إذا كان الرمز فارغًا) وإخطار المراقبين بالتغييرات. لذلك ، أرى الهدف ، أذهب إلى الهدف.


 const setToken = (token: typeof _token) => { if (token) { localStorage.setItem('REACT_TOKEN_AUTH', JSON.stringify(token)); } else { localStorage.removeItem('REACT_TOKEN_AUTH'); } _token = token; notify(); }; 

تأكد من أنك إذا وصلت إلى هذه النقطة في المقالة ووجدتها مفيدة ، فأنت بالفعل أسعدتني. هنا ننتهي مع موفر الرمز المميز. يمكنك إلقاء نظرة على الكود واللعب بها والتحقق من أنها تعمل. في الجزء التالي أعلاه ، سنقوم بإنشاء المزيد من الوظائف المجردة التي ستكون مفيدة بالفعل في أي تطبيق React.


مزود المصادقة


لنقم بإنشاء فئة جديدة من الكائنات التي سوف نسميها كموفر مصادقة. ستحتوي الواجهة على 4 طرق: ربط useAuth() للحصول على حالة جديدة من authFetch() component ، authFetch() لتقديم طلبات إلى الشبكة باستخدام الرمز المميز الفعلي وطرق login() ، logout() التي سوف تستدعي المكالمات إلى الأسلوب setToken() من موفر الرمز المميز (في هذه الحالة ، سيكون لدينا نقطة إدخال واحدة فقط للوظيفة التي تم إنشاؤها بالكامل ، ولن يتوجب على بقية الكود معرفة وجود موفر الرمز المميز). كما كان من قبل سنبدأ من مُنشئ الوظائف:


 export const createAuthProvider = () => { /* Implementation */ return { useAuth, authFetch, login, logout } }; 

بادئ ذي بدء ، إذا أردنا استخدام موفر الرمز المميز ، فنحن بحاجة إلى إنشاء مثيل منه:


 const tokenProvider = createTokenProvider(); 

طرق login() و logout() ببساطة تمرير الرمز المميز إلى موفر الرمز المميز. لقد فصلت هذه الطرق فقط عن المعنى الصريح (تمرير رمز مميز فارغ / فارغ بالفعل يزيل البيانات من التخزين المحلي):


 const login: typeof tokenProvider.setToken = (newTokens) => { tokenProvider.setToken(newTokens); }; const logout = () => { tokenProvider.setToken(null); }; 

والخطوة التالية هي وظيفة الجلب. وفقًا لفكرتي ، يجب أن تحتوي هذه الوظيفة على نفس واجهة المستخدم الأصلي تمامًا وأن تعيد التنسيق نفسه ولكن يجب أن تحقن رمز وصول لكل طلب.


يجب أن تأخذ دالة الجلب وسيطين: معلومات الطلب (عادةً URL) وطلب init (كائن مع الأسلوب ، النص الأساسي. الرؤوس وهكذا) ؛ وإرجاع الوعد للاستجابة:


 const authFetch = async (input: RequestInfo, init?: RequestInit): Promise<Response> => { const token = await tokenProvider.getToken(); init = init || {}; init.headers = { ...init.headers, Authorization: `Bearer ${token}`, }; return fetch(input, init); }; 

داخل الوظيفة قمنا بعمل شيئين: أخذنا رمزًا مميزًا من موفر الرمز المميز ببيان await tokenProvider.getToken(); (يحتوي getToken بالفعل على منطق تحديث الرمز المميز بعد انتهاء الصلاحية) وحقن هذا الرمز المميز في رأس Authorization بواسطة سطر Authorization: 'Bearer ${token}' . بعد ذلك ، نعود ببساطة بجلب الحجج المحدثة.


لذلك ، يمكننا بالفعل استخدام مزود المصادقة لحفظ الرموز واستخدامها من الجلب. المشكلة الأخيرة هي أنه لا يمكننا الرد على التغييرات الرمزية من مكوناتنا. الوقت لحلها.


كما قلت من قبل ، useAuth() هوك useAuth() يستخدم لتوفير معلومات للمكون هو المستخدم الذي قام بتسجيل الدخول أم لا. لتكون قادرًا على القيام بذلك ، سنستخدم خطاف useState() للاحتفاظ بهذه المعلومات. إنه مفيد لأن أي تغييرات في هذه الحالة سوف تتسبب في إعادة تقديم المكونات التي تستخدم هذا الخطاف.


وقد أعددنا بالفعل كل شيء لتتمكن من الاستماع إلى التغييرات في التخزين المحلي. هناك طريقة شائعة للاستماع إلى أي تغييرات في النظام مع السنانير تستخدم useEffect() . يستغرق هذا الخطاف وسيطين: الوظيفة وقائمة التبعيات. سيتم تشغيل الوظيفة بعد أول استدعاء useEffect ثم إعادة تشغيلها بعد أي تغييرات في قائمة التبعيات. في هذه الوظيفة ، يمكننا البدء في الاستماع إلى التغييرات في التخزين المحلي. ولكن من المهم أن نعود من هذه الوظيفة ... وظيفة جديدة ، وسيتم إطلاق هذه الوظيفة الجديدة إما قبل إعادة تشغيل أول وظيفة أو بعد إلغاء تثبيت المكون. في الوظيفة الجديدة ، يمكننا التوقف عن الاستماع إلى التغييرات وضمانات React ، والتي سيتم إطلاقها (على الأقل في حالة عدم حدوث استثناء أثناء هذه العملية). يبدو معقدا بعض الشيء ولكن مجرد إلقاء نظرة على الكود:


 const useAuth = () => { const [isLogged, setIsLogged] = useState(tokenProvider.isLoggedIn()); useEffect(() => { const listener = (newIsLogged: boolean) => { setIsLogged(newIsLogged); }; tokenProvider.subscribe(listener); return () => { tokenProvider.unsubscribe(listener); }; }, []); return [isLogged] as [typeof isLogged]; }; 

وهذا كل شيء. لقد أنشأنا للتو تخزين مصادقة رمزية مضغوطة وقابلة لإعادة الاستخدام مع واجهة برمجة تطبيقات واضحة. في الجزء التالي ، سننظر في بعض أمثلة الاستخدام.


استعمال


لبدء استخدام ما قمنا بتطبيقه أعلاه ، نحتاج إلى إنشاء مثيل لموفر المصادقة. سوف يتيح لنا الوصول إلى وظائف useAuth() ، authFetch() ، login() ، logout() المتعلقة بنفس الرمز المميز في التخزين المحلي (بشكل عام ، لا شيء يمنعك من إنشاء مثيلات مختلفة لموفر المصادقة لرموز مختلفة ، لكن ستحتاج إلى تحديد المفتاح الذي تستخدمه لتخزين البيانات في التخزين المحلي):


 export const {useAuth, authFetch, login, logout} = createAuthProvider(); 

نموذج تسجيل الدخول


الآن يمكننا البدء في استخدام الوظائف التي حصلنا عليها. لنبدأ بمكون نموذج تسجيل الدخول. يجب أن يوفر هذا المكون مدخلات لبيانات اعتماد المستخدم وحفظه في الحالة الداخلية. عند الإرسال ، نحتاج إلى إرسال طلب مع بيانات الاعتماد للحصول على الرموز المميزة ، وهنا يمكننا استخدام وظيفة login() لتخزين الرموز المستلمة:


 const LoginComponent = () => { const [credentials, setCredentials] = useState({ name: '', password: '' }); const onChange = ({target: {name, value}}: ChangeEvent<HTMLInputElement>) => { setCredentials({...credentials, [name]: value}) }; const onSubmit = (event?: React.FormEvent) => { if (event) { event.preventDefault(); } fetch('/login', { method: 'POST', body: JSON.stringify(credentials) }) .then(r => r.json()) .then(token => login(token)) }; return <form onSubmit={onSubmit}> <input name="name" value={credentials.name} onChange={onChange}/> <input name="password" value={credentials.password} onChange={onChange}/> </form> }; 

وهذا كل شيء ، إنه كل ما نحتاجه لتخزين الرمز. بعد ذلك ، عندما يتم تلقي رمز مميز ، لن نحتاج إلى بذل جهد إضافي لجلبه إلى جلب أو في مكونات ، لأنه قد تم تطبيقه بالفعل داخل موفر المصادقة.


نموذج التسجيل مشابه ، فهناك اختلافات فقط في عدد أسماء الحقول المدخلة وأسماءها ، لذلك سأحذفها هنا.


راوتر


أيضا ، يمكننا تنفيذ التوجيه باستخدام مزود المصادقة. لنفترض أن لدينا مجموعتين من الطرق: واحدة للمستخدم المسجل والآخر لغير المسجلين. useAuth() نحتاج إلى التحقق ، هل لدينا رمز مميز في التخزين المحلي أم لا ، وهنا يمكننا استخدام hook useAuth() :


 export const Router = () => { const [logged] = useAuth(); return <BrowserRouter> <Switch> {!logged && <> <Route path="/register" component={Register}/> <Route path="/login" component={Login}/> <Redirect to="/login"/> </>} {logged && <> <Route path="/dashboard" component={Dashboard} exact/> <Redirect to="/dashboard"/> </>} </Switch> </BrowserRouter>; }; 

والشيء الجميل أنه سيتم إعادة تقديمه بعد أي تغييرات في التخزين المحلي ، بسبب useAuth لديه اشتراك في هذه التغييرات.


جلب طلبات


ومن ثم يمكننا الحصول على البيانات المحمية بواسطة الرمز المميز باستخدام authFetch . له نفس واجهة الجلب ، لذلك إذا كنت تستخدم الجلب بالفعل في الكود ، يمكنك ببساطة استبداله بـ authFetch :


 const Dashboard = () => { const [posts, setPosts] = useState([]); useEffect(() => { authFetch('/posts') .then(r => r.json()) .then(_posts => setPosts(_posts)) }, []); return <div> {posts.map(post => <div key={post.id}> {post.message} </div>)} </div> }; 

ملخص


لقد فعلنا ذلك. لقد كانت رحلة مثيرة للاهتمام ، ولكن لديها أيضًا النهاية (ربما سعيدة).


لقد بدأنا بفهم مشاكل تخزين الرموز المميزة. بعد ذلك قمنا بتطبيق حل ونظرنا أخيرًا في أمثلة حول كيفية استخدامه في تطبيق React.


كما قلت من قبل ، يمكنك أن تجد تطبيقي على جيثب في المكتبة. يحل مشكلة عامة أكثر قليلاً ولا يضع افتراضات حول بنية الكائن مع الرموز المميزة أو كيفية تحديث الرمز المميز ، لذلك ستحتاج إلى تقديم بعض الوسائط الإضافية. لكن فكرة الحل هي نفسها ويحتوي المستودع أيضًا على إرشادات حول كيفية استخدامه.


هنا أستطيع أن أقول شكرا لك على قراءة المقال وآمل أن يكون مفيدا لك.

Source: https://habr.com/ru/post/ar485764/


All Articles