
في التطبيقات الحديثة التي تدعم المصادقة ، غالبًا ما نريد تغيير ما هو مرئي للمستخدم ، اعتمادًا على دوره. على سبيل المثال ، يمكن للمستخدم الضيف رؤية مقالة ، ولكن فقط المستخدم المسجل أو المسؤول يرى زرًا لحذف هذه المقالة.
يمكن أن تكون إدارة هذه الرؤية كابوسًا كاملاً مع زيادة الأدوار. ربما تكون قد كتبت أو رأيت رمزًا مثل هذا:
if (user.role === ADMIN || user.auth && post.author === user.id) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) }
يتم توزيع هذا الرمز في جميع أنحاء التطبيق وعادة ما يصبح مشكلة كبيرة عندما يغير العميل المتطلبات أو يطلب إضافة أدوار إضافية. في النهاية ، تحتاج إلى المرور بكل هذه القواعد وإضافة عمليات تحقق إضافية.
في هذه المقالة ، سأعرض طريقة بديلة لتنفيذ إدارة الأذونات في Expressjs API باستخدام مكتبة تسمى CASL . إنه يبسط إلى حد كبير التحكم في الوصول ويسمح لك بإعادة كتابة المثال السابق إلى شيء مثل هذا:
if (req.ability.can('read', post)) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) }
جديد على CASL؟ أوصي بقراءة ما هو CASL؟
ملاحظة: تم نشر هذا المقال في الأصل على Medium.
تطبيق تجريبي
كتطبيق اختباري ، قمت بعمل واجهة برمجة تطبيقات REST بسيطة إلى حد ما للمدونة . يتكون التطبيق من 3 كيانات ( User
Post
Comment
) و 4 وحدات (وحدة واحدة لكل كيان وأخرى للتحقق من التفويض). يمكن العثور على جميع الوحدات في المجلد src/modules
.
يستخدم التطبيق نماذج النمس ، ومصادقة passportjs والتفويض القائم على CASL (أو التحكم في الوصول). باستخدام واجهة برمجة التطبيقات ، يمكن للمستخدم:
- قراءة جميع المقالات والتعليقات
- إنشاء مستخدم (أي تسجيل)
- إدارة مقالاتك الخاصة (إنشاء ، تحرير ، حذف) ، إذا كان مصرحًا بذلك
- تحديث المعلومات الشخصية إذا أذن بذلك
- إدارة التعليقات الخاصة بك إذا سمحت
لتثبيت هذا التطبيق ، ما npm install
من github وتشغيل npm install
و npm start
. تحتاج أيضًا إلى تشغيل خادم mongodb://localhost:27017/blog
، حيث يتصل التطبيق بـ mongodb://localhost:27017/blog
. بعد أن يصبح كل شيء جاهزًا ، يمكنك اللعب قليلاً ، ولجعله أكثر متعة ، قم باستيراد البيانات الأساسية من المجلد db/
:
mongorestore ./db
بدلاً من ذلك ، يمكنك اتباع الإرشادات الموجودة في ملف مشروع README أو استخدام مجموعة Postman الخاصة بي.
ما هي الحيلة؟
أولاً ، الميزة الكبرى لـ CASL هي أنها تسمح لك بتحديد حقوق الوصول في مكان واحد لجميع المستخدمين! ثانيًا ، لا تركز CASL على من هو المستخدم ، ولكن على ما يمكنه القيام به ، أي على قدراتها. هذا يسمح لك بتوزيع هذه القدرات على الأدوار أو مجموعات المستخدمين المختلفة دون بذل مجهود غير ضروري. هذا يعني أنه يمكننا تسجيل حقوق الوصول للمستخدمين المصرح لهم وغير المصرح لهم:
const { AbilityBuilder, Ability } = require('casl') function defineAbilitiesFor(user) { const { rules, can } = AbilityBuilder.extract() can('read', ['Post', 'Comment']) can('create', 'User') if (user) { can(['update', 'delete', 'create'], ['Post', 'Comment'], { author: user._id }) can(['read', 'update'], 'User', { _id: user._id }) } return new Ability(rules) } const ANONYMOUS_ABILITY = defineAbilitiesFor(null) module.exports = function createAbilities(req, res, next) { req.ability = req.user.email ? defineAbilitiesFor(req.user) : ANONYMOUS_ABILITY next() }
دعونا الآن تحليل الرمز المكتوب أعلاه. يتم إنشاء مثيل من AbilityBuilder
-a في defineAbilitiesFor(user)
، وتقسم طريقة الاستخراج الخاصة به إلى تقسيم هذا الكائن إلى دالتين بسيطتين can
cannot
استخدام مجموعة من rules
( cannot
يمكن استخدامها في هذا الرمز). بعد ذلك ، باستخدام الاستدعاءات إلى وظيفة can
، نحدد ما يمكن للمستخدم القيام به: الوسيطة الأولى تمرر الإجراء (أو مجموعة من الإجراءات) ، الوسيطة الثانية هي نوع الكائن الذي يتم تنفيذ الإجراء عليه (أو صفيف من الأنواع) ، ويمكن تمرير كائن الشرط كوسيطة اختيارية ثالثة. يتم استخدام كائن الشرط عند التحقق من حقوق الوصول على مثيل للفئة ، أي يتحقق ما إذا كانت خاصية author
الكائن user._id
متساوية ، إذا كانت كذلك ، فسيتم إرجاع true
، وإلا فإن false
. للتوضيح ، سأعطي مثالاً:
// Post is a mongoose model const post = await Post.findOne() const user = await User.findOne() const ability = defineAbilitiesFor(user) console.log(ability.can('update', post)) // post.author === user._id, true
بعد ذلك ، باستخدام if (user)
نحدد حقوق الوصول للمستخدم المصرح له (إذا لم يكن المستخدم مصرحًا له ، فلا نعرف من هو وليس لدينا كائن يحتوي على معلومات حول المستخدم). في النهاية ، نعيد مثيل من فئة Ability
، وبمساعدة نتحقق من حقوق الوصول.
بعد ذلك ، نقوم بإنشاء ثابت ANONYMOUS_ABILITY
، وهو مثال على فئة Ability
للمستخدمين غير المصرح لهم. في النهاية ، نقوم بتصدير البرمجيات الوسيطة السريعة ، المسؤولة عن إنشاء مثيل Ability
لمستخدم معين.
اختبار API
دعونا نختبر ما حدث مع ساعي البريد . تحتاج أولاً إلى الوصول إلى TokenTone ، لإرسال طلب:
POST /session { "session": { "email": "casl@medium.com", "password": "password" } }
تحصل على شيء من هذا القبيل كرد فعل:
{ "accessToken": "...." }
يجب إدراج هذا الرمز المميز في Authorization header
وإرساله مع جميع الطلبات اللاحقة.
الآن دعنا نحاول تحديث المقالة.
PATCH http://localhost:3030/posts/597649a88679237e6f411ae6 { "post": { "title": "[UPDATED] my post title" } } 200 Ok { "post": { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } }
كل شيء يعمل بشكل جيد. ولكن ماذا لو قمنا بتحديث مقالة شخص آخر؟
PATCH http://localhost:3030/posts/59761ba80203fb638e9bd85c { "post": { "title": "[EVIL ACTION] my post title" } } 403 { "status": "forbidden", "message": "Cannot execute \"update\" on \"Post\"" }
حصل خطأ! كما هو متوقع :)
الآن دعونا نتخيل أنه بالنسبة لمؤلفي مدونتنا ، نريد إنشاء صفحة يمكنهم من خلالها مشاهدة جميع المشاركات التي يمكنهم تحديثها. من وجهة نظر منطق معين ، هذا ليس صعبًا ، ما عليك سوى تحديد جميع المقالات التي يساوي المؤلف فيها user._id
. لكننا قمنا بالفعل بتسجيل مثل هذا المنطق بمساعدة CASL ، سيكون من الملائم جدًا الحصول على جميع هذه المقالات من قاعدة البيانات دون كتابة طلبات إضافية ، وإذا تغيرت الحقوق ، فيجب تغيير الطلب - عمل إضافي :).
لحسن الحظ ، لدى CASL حزمة npm إضافية - @ casl / mongoose . تتيح لك هذه الحزمة الاستعلام عن السجلات من MongoDB وفقًا لأذونات محددة! بالنسبة للنمس ، توفر هذه الحزمة مكونًا إضافيًا يضيف طريقة accessibleBy(ability, action)
إلى النموذج. باستخدام هذه الطريقة ، سنطلب أيضًا السجلات من قاعدة البيانات (اقرأ المزيد حول هذا في وثائق CASL وملف حزمة README ).
هذه هي الطريقة التي يتم بها تنفيذ handler
/posts
النشرات (أضفت أيضًا القدرة على تحديد الإجراء الذي تحتاجه للتحقق من الأذونات):
Post.accessibleBy(req.ability, req.query.action)
لذا ، من أجل حل المشكلة الموضحة سابقًا ، ما عليك سوى إضافة action=update
المعلمة action=update
:
GET http://localhost:3030/posts?action=update 200 Ok { "posts": [ { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } ] }
في الختام
بفضل CASL ، لدينا طريقة جيدة حقًا لإدارة حقوق الوصول. أنا متأكد من أن نوع البناء
if (ability.can('read', post)) ...
أكثر وضوحا وأسهل من ذلك
if (user.role === ADMIN || user.auth && todo.author === user.id) ...
مع CASL ، يمكننا أن نكون أكثر وضوحًا حول ما يفعله الكود الخاص بنا. بالإضافة إلى ذلك ، من المؤكد أنه سيتم استخدام عمليات التحقق هذه في مكان آخر في تطبيقنا ، وهنا ستساعد CASL على تجنب تكرار التعليمات البرمجية.
آمل أن تكون مهتمًا بالقراءة عن CASL كما كنت مهتمًا بإنشائه. لدى CASL وثائق جيدة جدًا ، ربما ستجد الكثير من المعلومات المفيدة هناك ، ولكن لا تتردد في طرح الأسئلة إذا كانت في دردشة gitter وإضافة علامة نجمية على github ؛)