مرة أخرى حول جواز السفر

في الآونة الأخيرة ، قدموا لي الدعم لمشروع على Express.js. عند دراسة رمز المشروع ، وجدت عملًا مربكًا بعض الشيء مع المصادقة / التخويل ، والذي كان يستند ، مثل 99.999٪ من الحالات ، إلى مكتبة passport.js. عملت هذه الشفرة ، وتبعًا لمبدأ "العمل - لا تلمس" ، تركتها كما هي. عندما تم تكليفي بعد يومين بمهمة إضافة استراتيجيتين أخريين للترخيص. ثم بدأت أتذكر أنني كنت أقوم بالفعل بعمل مماثل ، واستغرق الأمر عدة سطور من التعليمات البرمجية. بعد الاطلاع على الوثائق الخاصة بجواز السفر ، لم أكن أتحرك في فهم ماذا وكيف نفعل ، لأنه هناك نظروا في الحالات عندما يتم استخدام استراتيجية واحدة بالضبط ، والتي ، لكل فرد ، يتم إعطاء أمثلة. ولكن كيف تجمع بين العديد من الاستراتيجيات ، لماذا تحتاج إلى استخدام طريقة تسجيل الدخول () التي تشبه طريقة تسجيل الدخول ()) - لا تزال غير موضحة. لذلك ، لكي أفهم الآن ، وليس تكرار البحث نفسه مرارًا وتكرارًا ، قمت بتجميع هذه الملاحظات بنفسي.

قليلا من التاريخ. في البداية ، استخدمت تطبيقات الويب نوعين من المصادقة / التخويل: 1) أساسي و 2) باستخدام جلسات باستخدام ملفات تعريف الارتباط. في المصادقة / التخويل الأساسي ، يتم إرسال رأس في كل طلب ، وبالتالي يتم إجراء مصادقة العميل في كل طلب. عند استخدام الجلسات ، يتم إجراء مصادقة العميل مرة واحدة فقط (يمكن أن تكون الطرق مختلفة جدًا ، بما في ذلك Basic ، وكذلك حسب الاسم وكلمة المرور ، التي يتم إرسالها في النموذج ، وآلاف الطرق الأخرى التي تسمى الاستراتيجيات من حيث passport.js). الشيء الرئيسي هو أنه بعد المصادقة ، يحدد العميل معرف الجلسة في ملف تعريف الارتباط (أو في بعض التطبيقات ، بيانات الجلسة) ، ويتم تخزين معرف المستخدم في بيانات الجلسة.

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

const app = express(); const sessionMiddleware = session({ store: new RedisStore({client: redisClient}), secret, resave: true, rolling: true, saveUninitialized: false, cookie: { maxAge: 10 * 60 * 1000, httpOnly: false, }, }); app.use(cookieParser()); app.use(sessionMiddleware); app.use(passport.initialize()); app.use(passport.session()); 

هنا تحتاج إلى إعطاء بعض التفسيرات المهمة. إذا كنت لا ترغب في أن يقوم redis بتناول جميع ذاكرة الوصول العشوائي (RAM) في غضون عامين ، فيجب عليك العناية بحذف بيانات الجلسة في الوقت المناسب. تعتبر المعلمة maxAge مسؤولة عن هذا الأمر ، حيث تقوم أيضًا بتعيين هذه القيمة لكل من ملف تعريف الارتباط والقيمة المخزنة في redis. يؤدي تعيين القيم إلى إعادة حفظ: true ، rolling: true ، إلى تمديد فترة الصلاحية مع قيمة maxAge المحددة مع كل طلب جديد (إذا لزم الأمر). وإلا ، سيتم مقاطعة جلسة العميل بشكل دوري. وأخيرًا ، فإن saveUninitialized: المعلمة false لن تضع جلسات فارغة في redis. يتيح لك ذلك وضع تهيئة الجلسات و passport.js على مستوى التطبيق ، دون انسداد redis بالبيانات غير الضرورية. على مستوى المسار ، من المنطقي وضع التهيئة فقط إذا كانت طريقة passport.initialize () بحاجة إلى استدعاء بمعلمات مختلفة.

إذا لم يتم استخدام الجلسة ، فسيتم التهيئة بشكل كبير:

 app.use(passport.initialize()); 


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

 const jwtStrategy = new JwtStrategy(params, (payload, done) => UserModel.findOne({where: {id: payload.userId}}) .then((user = null) => { done(null, user); }) .catch((error) => { done(error, null); }) ); 


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

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

 passport.use('jwt', jwtStrategy); passport.use('simple-jwt', simpleJwtStrategy); 


بعد ذلك ، بالنسبة إلى المسار المحمي ، تحتاج إلى تعيين استراتيجية مصادقة ومعلمة جلسة مهمة (الافتراضي صحيح):

 const authenticate = passport.authenticate('jwt', {session: false}); router.use('/hello', authenticate, (req, res) => { res.send('hello'); }); 


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

 const authenticate = passport.authenticate('local', {session: true}); router.post('/login', authenticate, (req, res) => { res.send({}) ; }); router.post('/logout', mustAuthenticated, (req, res) => { req.logOut(); res.send({}); }); 


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

 function mustAuthenticated(req, res, next) { if (!req.isAuthenticated()) { return res.status(HTTPStatus.UNAUTHORIZED).send({}); } next(); } 


لذلك ، كانت هناك لحظة أخيرة - إجراء تسلسل وإلغاء تسلسل كائن request.user إلى / من الجلسة:

 passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser((id, done) => { UserModel.findOne({where: {id}}).then((user) => { done(null, user); return null; }); }); 


أريد التأكيد مرة أخرى على أن التسلسل وإلغاء التسلسل لا يعملان إلا مع الاستراتيجيات التي تم تعيين السمة {session: true} عليها. سيتم تنفيذ التسلسل مرة واحدة فورًا بعد المصادقة. لذلك ، سيكون تحديث البيانات المخزنة في الجلسة مشكلة كبيرة ، حيث يتم حفظ معرف المستخدم فقط (الذي لا يتغير). سيتم تنفيذ عملية إلغاء التسلسل عند كل طلب إلى طريق آمن. في هذا الصدد ، تؤثر استعلامات قاعدة البيانات (كما في المثال) بشكل كبير على أداء التطبيق.

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

هذا يمكن أن يكون نهاية القصة. بسبب بالإضافة إلى ما قيل ، في الممارسة العملية ، لا شيء آخر مطلوب. ومع ذلك ، بناءً على طلب مطوري الواجهة الأمامية ، اضطررت إلى إضافة كائن مع وصف الخطأ إلى استجابة 401 (افتراضيًا ، هذا هو الخط "غير مصرح به"). وهذا ، كما اتضح ، لا يمكن القيام به ببساطة. في مثل هذه الحالات ، تحتاج إلى التعمق أكثر في قلب المكتبة ، وهو ليس لطيفًا جدًا. تحتوي طريقة passport.authenticate على معلمة اختيارية ثالثة: وظيفة رد اتصال مع وظيفة التوقيع (خطأ ، مستخدم ، معلومات). المشكلة الصغيرة هي أنه لا يتم تمرير كائن الاستجابة ولا أي وظيفة مثل done () / next () إلى هذه الوظيفة ، وبالتالي يجب عليك تحويلها إلى برنامج وسيط بنفسك:

 route.post('/hello', authenticate('jwt', {session: false}), (req, res) => { res.send({}) ; }); function authenticate(strategy, options) { return function (req, res, next) { passport.authenticate(strategy, options, (error, user , info) => { if (error) { return next(error); } if (!user) { return next(new TranslatableError('unauthorised', HTTPStatus.UNAUTHORIZED)); } if (options.session) { return req.logIn(user, (err) => { if (err) { return next(err); } return next(); }); } req.user = user; next(); })(req, res, next); }; } 


روابط مفيدة:

1) toon.io/understanding-passportjs- مصادقة- تدفق
2) habr.com/post/201206
3) habr.com/company/ruvds/blog/335434
4) habr.com/post/262979
5) habr.com/company/Voximplant/blog/323160
6) habr.com/company/dataart/blog/262817
7) tools.ietf.org/html/draft-ietf-oauth-pop-architecture-08
8) oauth.net/articles/ مصادقة

apapacy@gmail.com
4 يناير 2019

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


All Articles