تُستخدم واجهات برمجة التطبيقات APIs على نطاق واسع في شتى المجالات، إذ تتيح للبرمجيات إمكانية التواصل والتكامل مع أنظمة وبرمجيات أخرى، سواء كانت داخلية ضمن نفس النظام أو خارجية. وتكمن إحدى أبرز فوائدها في تسهيل إعادة استخدام المكونات البرمجية، وتوفّر معظم الخدمات الإلكترونية عبر الإنترنت واجهات برمجة تطبيقات تمكّن المطورين من دمج ميزات متنوعة، مثل تسجيل الدخول واستخدام حسابات التواصل الاجتماعي، والدفع بواسطة بطاقات الائتمان، وتتبع سلوك المستخدمين بكل سهولة. ويُعد معيار نقل الحالة التمثيلية Representational State Transfer أو REST اختصارًا المعيار الأكثر شيوعًا في تصميم هذه الواجهات.
سنشرح في هذا المقال طريقة إنشاء REST API خاصة ببيئة Node.js فلغة جافا سكريبت JavaScript من أكثر لغات البرمجة شيوعًا بين المطورين المحترفين. لذلك سنركز في هذا المقال على إنشاء REST API بسيطة وآمنة، ويمكن بالطبع استخدام أي من المنصات واللغات البرمجية الأخرى لتحقيقها، مثل إطار عمل ASP.NET Core، أو إطار لارافيل Laravel للغة PHP، أو إطار Bottle للغة Python لإنشاء واجهة REST API.
متطلبات العمل
يحتاج فهم وتطبيق هذا المقال لامتلاك الأمور التالية:
- خبرة مسبقة في التعامل مع إطار Node.js
- معرفة بإطار Express.js لأنه معيار أساسي في بناء الواجهة الخلفية للواجهة البرمجية REST API
- معرفة بمكتبة Mongoose لربط الواجهة الخلفية بقاعدة بيانات MongoDB
ملاحظة: لن نتناول في هذا المقال شرح أكواد الواجهة الأمامية، وسنركز على الواجهة الخلفية فقط. ولكن بما أننا سنستخدم لغة JavaScript في تطوير الواجهة الخلفية، فإن هذا يتيح لنا إمكانية مشاركة بعض الأجزاء البرمجية مثل نماذج الكائنات Object Models بين الواجهة الأمامية والخلفية، مما يسهم في توحيد الكود وتقليل التكرار داخل المشروع.
هيكلية REST API
تستخدم واجهات REST APIs من أجل الوصول للبيانات والتحكم بها، وذلك باستخدام مجموعة من العمليات عديمة الحالة stateless بمعنى أن الخادم ينفذ العملية المطلوبة منه ثم ينسى كل شيء عنها بعد الانتهاء منها. وهذه العمليات جزء لا يتجزأ من بروتوكول HTTP المستخدم في نقل البيانات على الويب، وهي تستخدم في تنفيذ الوظائف الأساسية كإنشاء البيانات وقراءتها وتحديثها وحذفها أو ما يعرف باسم عمليات CRUD، على الرغم من عدم تطابقها بدقة تامة معها. وهذه العمليات هي كالتالي:
-
POST
لإنشاء مورد جديد أو توفير البيانات بشكل عام -
GET
للحصول على قائمة موارد أو مورد محدد -
PUT
لإنشاء أو استبدال مورد -
PATCH
لتحديث أو تعديل مورد -
DELETE
لحذف المورد
ملاحظة: ما نعنيه بالمورد resource هنا البيانات التي نتعامل معها عبر واجهة برمجة التطبيقات API ويمكننا الوصول إليها عبر عنوان URL لنقطة الوصول endpoint.
يتيح لنا بناء واجهة REST API باستخدام Node.js إنشاء نقاط وصول Endpoints لكل عملية من عمليات التحكم بالبيانات، وذلك من خلال استخدام بروتوكول HTTP مع تحديد اسم المورد ضمن عنوان URL. عند اتباع هذا الأسلوب، سنحصل على هيكل واضح ومستقر للواجهة البرمجية يسهل فهمه وتطويره وصيانته بمرونة وسرعة. كما أن هذا النمط يُعد شائعًا بين معظم الخدمات الخارجية التي تعتمد بدورها على REST API، مما يُسهّل عملية التكامل معها لاحقًا، سواء لإضافة ميزات مثل تسجيل الدخول أو الدفع الإلكتروني.
والآن، دعونا نبدأ بشرح خطوات بناء REST API آمنة باستخدام Node.js خطوة بخطوة.
بناء REST API آمنة لإدارة المستخدمين باستخدام Node.js
سنبني في هذا المقال REST API آمنة تتميز بكونها شائعة وعملية لإدارة المستخدمين users
. سيملك المورد الذي سننشئه هيكلا بسيطا كالآتي:
-
id
: معرف مستخدم مميز UUID منشأ تلقائيًا -
:
firstName
: الاسم الأول للمستخدم -
lastName
: الاسم الأخير للمستخدم -
email
: البريد الإلكتروني للمستخدم -
password
: كلمة المرور -
permissionLevel
: أذونات المستخدم كأن يكون مستخدم عادي أو مشرف أو لديه أذونات خاصة
ثم سننشئ عمليات لهذا المورد كالآتي:
-
POST
عند نقطة الوصول/users
لإنشاء مستخدم جديد -
GET
عند نقطة الوصول/users
للحصول على قائمة بكافة المستخدمين -
GET
عند نقطة الوصول/users/:userId
للحصول على بيانات مستخدم معين -
PATCH
عند نقطة الوصول/users/:userId
لتحديث بيانات مستخدم معين -
DELETE
عند نقطة الوصول/users/:userId
لحذف مستخدم معين
سنستخدم رموز JSON Web Tokens -أو JWTs اختصارًا- كوسلية للتحقق من هوية المستخدمين. عند تسجيل الدخول، سيُطلب من المستخدم إدخال بريده الإلكتروني وكلمة مروره، وسيتم إرسال هذه البيانات إلى مورد خاص بالمصادقة يحمل الاسم auth
، بعد التحقق من صحة البيانات، يصدر هذا المورد رمز JWT ويُستخدم لاحقًا للوصول إلى بعض العمليات المحمية داخل الواجهة البرمجية. لمعرفة المزيد عن هذا النوع من الرموز، يمكن الرجوع إلى مقال الاستيثاق عبر مفتاح المستخدم المشفر token authentication في تطبيق Node.js و React
إعداد بيئة العمل
بداية، علينا التأكد من تثبيت آخر إصدار من Node.js على جهازنا، يستخدم في هذا المقال الإصدار 14.9.0 لكن يمكنك استخدام إصدار أحدث إذا كان متوفرًا. بعد ذلك يجب التأكد من أن MongoDB مثبت أيضَا على جهازنا، لن نخوض في تفاصيل استخدام Mongoose أو هيكلية MongoDB، لكننا سنعتمد على تشغيل الأساسيات فقط.
لتبسيط العمل، يمكن تشغيل خادم MongoDB في الوضع التفاعلي، أي باستخدام سطر الأوامر مباشرة بدلًا من تشغيله كخدمة تعمل في الخلفية بكتابة الأمر التالي:
mongo
سنحتاج لاحقًا في هذا المقال إلى التفاعل المباشر مع MongoDB من خلال هذا الوضع -دون استخدام كود Node.js- لفهم كيفية التعامل مع البيانات.
ملاحظة: عند استخدام MongoDB، لا نحتاج لإنشاء قاعدة بيانات محددة كما هو الحال في بعض أنظمة إدارة قواعد البيانات العلاقية RDBMS فسيؤدي أول استدعاء لها من كود Node.js إلى إنشائها تلقائيًا إذا لم تكن موجودة، مما يسهل العمل ويسرع عملية التطوير.
لا يحتوي هذا المقال على جميع الأكواد اللازمة لاكتمال عمل المشروع، ويمكنك الحصول على الكود كاملًا من هذا المستودع ومتابعة النقاط المهمة أثناء القراءة، كما يمكن نسخ ملفات ومقاطع محددة من المستودع. بعد ذلك سننتقل إلى ملف rest-api-tutorial/
الناتج في الطرفية، نلاحظ أن المشروع يحتوي على ثلاثة مجلدات لوحدات Modules تنظم مختلف أجزاء التطبيق وهي كالتالي:
-
common
لمعالجة جميع الخدمات المشتركة والمعلومات المتبادلة بين المستخدمين -
users
يتضمن كل ما يتعلق بإدارة المستخدمين -
auth
لمعالجة رموز JWT وعمليات تسجيل الدخول
بعد ذلك، سنشغل الأمر npm install
أو الأمر yarn
لتنصيب التبعيات المطلوبة. بمجرد اكتمال عملية التنصيب، سنكون قد أعددنا كل الأمور والتبعيات اللازمة لتشغيل الواجهة الخلفية للواجهة البرمجية REST API باستخدام Node.js.
إنشاء وحدة المستخدم User Module
سنستخدم مكتبة Mongoose مع MongoDB ، وهي مكتبة تساعدنا في التعامل مع بيانات MongoDB بطريقة مبسطة، وذلك من خلال إنشاء مخطط schema يحدد شكل بيانات المستخدم، ثم استخدامه لبناء نموذج model يسمح لنا بإضافة المستخدمين وتعديلهم وحذفهم من قاعدة البيانات.
أولًا، نحتاج لإنشاء مخطط المستخدم User Schema في الملف /users/models/users.model.js
كما يلي:
const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, permissionLevel: Number });
بعد تعريف المخطط يمكننا ربط المخطط بنموذج المستخدم بسهولة كما يلي:
const userModel = mongoose.model('Users', userSchema);
يمكننا بعدها استخدام هذا النموذج لإضافة كل عمليات CRUD التي نحتاجها في نقاط وصول Express.js الخاصة بنا.
لنبدأ بإنشاء مستخدم جديد باستخدام العملية create user
عن طريق تعريف وجهة Route في الملف users/routes.config.js
:
app.post('/users', [ UsersController.insert ]);
نقوم بإدراج هذا الجزء من الكود داخل تطبيقنا باستخدام Express.js من خلال الملف الرئيسي index.js
، أما كائن UsersController
، فنستورده من ملف المتحكم controller المسؤول عن معالجة المستخدمين.
وسنشفر كلمة المرور بالشكل المناسب في الملف /users/controllers/user.controller.js
لضمان حفظها بأمان في قاعدة البيانات:
exports.insert = (req, res) => { let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512',salt) .update(req.body.password) .digest("base64"); req.body.password = salt + "$" + hash; req.body.permissionLevel = 1; UserModel.createUser(req.body) .then((result) => { res.status(201).send({id: result._id}); }); };
عند الوصول لهذه النقطة يمكننا اختبار نموذجنا Mongoose عن طريق تشغيل خادم Node.js API باستخدام الأمر npm start
وإرسال طلب POST
يحتوي على بعض بيانات JSON إلى نقطة الوصول users/
:
{ "firstName" : "ِِAbd", "lastName" : "Hamza", "email" : "ِِabd.hamza@example.com", "password" : "s3cr3tp4sswo4rd" }
هنالك العديد من الأدوات التي يمكننا استخدامها لاختبار واجهتنا البرمجية، سنعتمد على الأداة Insomnia، و يمكن أيضًا استخدام Postman أو أي بدائل مفتوحة المصدر مثل أداة cURL أو Bruno كما يمكننا الاكتفاء بكتابة أكواد جافاسكريبت مباشرة، ككتابة الكود التالي في التبويب Console ضمن أدوات المطور Developer Tools للمتصفح:
fetch('http://localhost:3600/users', { method: 'POST', headers: { "Content-type": "application/json" }, body: JSON.stringify({ "firstName": "Abd", "lastName": "Hamza", "email": "abd.hamza@example.com", "password": "s3cr3tp4sswo4rd" }) }) .then(function(response) { return response.json(); }) .then(function(data) { console.log('Request succeeded with JSON response', data); }) .catch(function(error) { console.log('Request failed', error); });
ستحتوي نتيجة طلب POST
الناجحة على المعرف id
للمستخدم الذي أنشأناه حديثًا:
{ "id": "5b02c5c84817bf28049e58a3" }
سنحتاج أيضا لإضافة التابع createUser
إلى النموذج في الملف users/models/users.model.js
:
exports.createUser = (userData) => { const user = new User(userData); return user.save(); };
الآن علينا التأكد فيما إذا كان المستخدم موجودًا في قاعدة البيانات، لذلك سنضيف ميزة الحصول على اسم المستخدم من خلال المعرّف id
إلى نقطة الوصول users/:userId
.
سننشئ أولًا وجهة Route للتطبيق في الملف /users/routes/config.js
:
app.get('/users/:userId', [ UsersController.getById ]);
ثم سننشئ وحدة تحكم في الملف /users/controllers/users.controller.js
:
exports.getById = (req, res) => { UserModel.findById(req.params.userId).then((result) => { res.status(200).send(result); }); };
وأخيرا نضيف التابع findById
إلى النموذج في الملف /users/models/users.model.js
:
exports.findById = (id) => { return User.findById(id).then((result) => { result = result.toJSON(); delete result._id; delete result.__v; return result; }); };
يجب أن نحصل على استجابة كما يلي:
{ "firstName": "Abd", "lastName": "Hamza", "email": "abd.hamza@example.com", "password": "Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==", "permissionLevel": 1, "id": "5b02c5c84817bf28049e58a3" }
لاحظ أن بإمكاننا رؤية كلمة المرور المشفرة هنا، ولكن يفضل عدم عرض كلمة المرور مطلقًا حتى ولو شفرناها. كما نلاحظ وجود الحقل permissionLevel
وسنستخدمه لاحقًا لتحديد ومعالجة أذونات المستخدم.
يمكننا الآن إضافة الوظيفة اللازمة لتحديث بيانات المستخدم مثل الاسم أو البريد الإلكتروني أو كلمة المرور بنفس الطريقة التي استخدمناها سابقًا. سنستخدم هنا عملية PATCH
لأنها تسمح لنا بإرسال الحقول المرغوب بتعديلها فقط، ولذلك ستكون الوجهة Route هو العملية PATCH
لنقطة الوصول
/users/:userid
وسنرسل أي حقول نريد تعديلها في جسم الطلب.
سنحتاج أيضًا لإضافة بعض التحققات الإضافية لأن التعديل يجب أن يقتصر على المستخدم المسؤول admin، ولا يجب أن يتمكن أحد غير المسؤول من التعديل على الحقل permisionLevel
. ولكننا سنتخطى هذا الأمر حاليًا ونعود له عندما نضيف وحدة المصادقة auth
. يجب أن يبدو المتحكم controller الخاص بنا الآن كما يلي:
exports.patchById = (req, res) => { if (req.body.password){ let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); req.body.password = salt + "$" + hash; } UserModel.patchUser(req.params.userId, req.body).then((result) => { res.status(204).send({}); }); };
ترسل هذه الخطوة استجابة برمز الحالة HTTP 204 الذي يشير لنجاح تنفيذ الطلب، ولكن دون إرجاع أي بيانات أو محتوى في جسم الاستجابة.
سنحتاج أيضا لإضافة التابع patchUser
للنموذج model لتحديث بيانات المستخدم:
exports.patchUser = (id, userData) => { return User.findOneAndUpdate({ _id: id }, userData); };
يحقق المتحكم controller التالي عملية GET
لعرض قائمة المستخدمين عند نقطة الوصول/users/
مع تطبيق آلية التصفية باستخدام الصفحات والحد الأقصى للعناصر المعروضة:
exports.list = (req, res) => { let limit = req.query.limit && req.query.limit <= 100 ? parseInt(req.query.limit) : 10; let page = 0; if (req.query) { if (req.query.page) { req.query.page = parseInt(req.query.page); page = Number.isInteger(req.query.page) ? req.query.page : 0; } } UserModel.list(limit, page).then((result) => { res.status(200).send(result); }) };
سيكون تابع النموذج المقابل كالآتي:
exports.list = (perPage, page) => { return new Promise((resolve, reject) => { User.find() .limit(perPage) .skip(perPage * page) .exec(function (err, users) { if (err) { reject(err); } else { resolve(users); } }) }); };
وستكون للاستجابة الناتجة البنية التالية:
[ { "firstName": "Maher", "lastName": "Saleh", "email": "maher.saleh@example.com", "password": "z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==", "permissionLevel": 1, "id": "5b02c5c84817bf28049e58a3" }, { "firstName": "Abd", "lastName": "Hamza", "email": "abd.hamza@example.com", "password": "wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==", "permissionLevel": 1, "id": "5b02d038b653603d1ca69729" } ]
وأخيرا سنضيف عملية DELETE
لحذف المستخدم بناء على المعرف الخاص به عند نقطة الوصول /users/:userId.
سننشئ المتحكم controller المسؤول عن الحذف كالآتي:
exports.removeById = (req, res) => { UserModel.removeById(req.params.userId) .then((result)=>{ res.status(204).send({}); }); };
كما في السابق، سيرسل المتحكم استجابة بالحالة HTTP 204 الذي يشير لنجاح الطلب. ويكون تابع النموذج model method المقابل كالآتي:
exports.removeById = (userId) => { return new Promise((resolve, reject) => { User.deleteMany({_id: userId}, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); };
أصبحنا الآن نملك جميع العمليات اللازمة للتحكم ببيانات المستخدم.
إنشاء وحدة المصادقة Auth
قبل أن نتمكن من تأمين وحدة المستخدمين users
عن طريق إضافة الأذونات والبرامج الوسيطة للتحقق، نحتاج أولًا إلى طريقة تمكننا من توليد رمز مميز JWT صالح للمستخدم الحالي. سيُستخدم هذا الرمز لإثبات هوية المستخدم في الطلبات المستقبلية دون الحاجة لإجراء التحقق في كل مرة. يملك هذا الرمز عادة وقت انتهاء صلاحية ويُنشَأ رمز جديد كل عدة دقائق للحفاظ على تواصل آمن، ولكن في هذا المقال سنتخطى إعادة إنشاء الرمز كل مرة ونكتفي برمز واحد كل تسجيل دخول لتبسيط العمل.
سننشئ بداية نقطة وصول تستقبل طلبات POST
إلى المورد auth/
. يجب أن يحتوي جسم الطلب على البريد الإلكتروني وكلمة المرور الخاصة بالمستخدم على النحو التالي:
{ "email" : "abd.hamza@example.com", "password" : "s3cr3tp4sswo4rd2" }
قبل أن نسمح للطلب بالوصول للمتحكم والتفاعل معه يجب التحقق من أن المستخدم موجود وكلمة مروره صحيحة عن طريق كود يكتب في الملف
/authorization/middlewares/verify.user.middleware.js
:
exports.isPasswordAndUserMatch = (req, res, next) => { UserModel.findByEmail(req.body.email) .then((user)=>{ if(!user[0]){ res.status(404).send({}); }else{ let passwordFields = user[0].password.split('$'); let salt = passwordFields[0]; let hash = crypto.createHmac('sha512', salt) .update(req.body.password) .digest("base64"); if (hash === passwordFields[1]) { req.body = { userId: user[0]._id, email: user[0].email, permissionLevel: user[0].permissionLevel, provider: 'email', name: user[0].firstName + ' ' + user[0].lastName, }; return next(); } else { return res.status(400).send({errors: ['Invalid email or password']}); } } }); };
بعد القيام بذلك، يمكننا الانتقال إلى المتحكم controller وتوليد رمز JWT:
exports.login = (req, res) => { try { let refreshId = req.body.userId + jwtSecret; let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(refreshId).digest("base64"); req.body.refreshKey = salt; let token = jwt.sign(req.body, jwtSecret); let b = Buffer.from(hash); let refresh_token = b.toString('base64'); res.status(201).send({accessToken: token, refreshToken: refresh_token}); } catch (err) { res.status(500).send({errors: err}); } };
رغم أننا في هذا المقال لن نعيد إنشاء الرمز المميز JWT بشكل متكرر، إلا أن المتحكم Controller مُصمم ليكون قادرًا على توليد هذه الرموز عند الحاجة. يُعد JWT جزءًا أساسيًا من عملية التحقق من هوية المستخدم، لذلك تم تضمين وظيفة توليد الرمز في المتحكم لتسهيل استخدامها مستقبلًا، سواء في هذا المشروع أو في مشاريع قادمة.
كل ما نحتاجه هو إنشاء وجهة واستدعاء البرمجية الوسيطة Middleware المناسبة في الملف /authorization/routes.config.js
على النحو التالي:
app.post('/auth', [ VerifyUserMiddleware.hasAuthValidFields, VerifyUserMiddleware.isPasswordAndUserMatch, AuthorizationController.login ]);
ستحتوي الاستجابة الناتجة على رمز JWT المولد في الحقل accessToken
:
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY", "refreshToken": "U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ==" }
بعد إنشاء الرمز، يمكننا استخدامه داخل ترويسة Authorization
باستخدام صيغة Bearer ACCESS_TOKEN
إنشاء برمجيات وسيطة لعمليات التحقق Validations والأذونات Permissions
أول ما يجب علينا فعله هو تحديد من يستطيع استخدام المورد users
. وفيما يلي السيناريوهات التي علينا التعامل معها:
- التسجيل أو إنشاء مستخدم جديد: متاح للجميع، ولا يتطلب استخدام رموز JWT
- تحديث بيانات مستخدم: مسموح فقط للمستخدم الذي قام بتسجيل الدخول أو للمسؤولين
- حذف حساب مستخدم: مخصص للمسؤولين فقط
بعد تحديد السيناريوهات المحتملة لكيفية تصميم مستويات مختلفة من الوصول بناءً على الأدوار ومتطلبات الأمان في التطبيق، سنحتاج أولًا إلى برمجية وسيطة Middleware تتحقق فيما إذا كان المستخدم يملك رمز JWT صالح. قد تكون عبارة عن برمجية وسيطة بسيطة ضمن الملف
/common/middlewares/auth.validation.middleware.js
كما يلي:
exports.validJWTNeeded = (req, res, next) => { if (req.headers['authorization']) { try { let authorization = req.headers['authorization'].split(' '); if (authorization[0] !== 'Bearer') { return res.status(401).send(); } else { req.jwt = jwt.verify(authorization[1], secret); return next(); } } catch (err) { return res.status(403).send(); } } else { return res.status(401).send(); } };
سنستخدم رموز الحالة الخاصة ببروتوكول HTTP للتعامل مع الأخطاء التي قد تحدث أثناء معالجة الطلبات، وذلك على النحو التالي:
- رمز 401 Unauthorized: يُستخدم عندما يكون الطلب غير مصحوب برمز JWT صالح، أو عندما لا يتم إرسال أي رمز على الإطلاق
- رمز 403 Forbidden: يُستخدم عندما يكون الرمز المرسل صالحًا، لكن المستخدم لا يملك الصلاحيات الكافية لتنفيذ هذا الطلب
يمكننا استخدام العملية المنطقية AND أو ما يُعرف بـ قناع البِت Bitmasking، حيث نعتبر كل بت في عدد مكوّن من 32 بت كإذن منفصل. وكل إذن يُمثَّل كقوة للعدد 2. على سبيل المثال، يمكن للمسؤول أن يمتلك جميع الأذونات عن طريق تعيين الرقم 2147483647، مما يعني أن جميع الأذونات مفعّلة. من جهة أخرى، يمكننا تعيين قيمة 7 للأذونات، مما يعني تفعيل الأذونات الموجودة في البتات التي تحمل القيم 1 و 2 و 4، أو ما يعادل القوى 0 و 1 و 2 للعدد 2.
وفيما يلي مثال على كيفية استخدام ذلك في برمجية وسيطة Middleware:
exports.minimumPermissionLevelRequired = (required_permission_level) => { return (req, res, next) => { let user_permission_level = parseInt(req.jwt.permission_level); let user_id = req.jwt.user_id; if (user_permission_level & required_permission_level) { return next(); } else { return res.status(403).send(); } }; };
تتحقق هذه البرمجية الوسيطة مما إذا كان مستوى أذونات المستخدم يتطابق مع المستوى المطلوب باستخدام العملية AND. إذا كانت النتيجة أكبر من الصفر -أي أن المستخدم يملك الإذن المطلوب- يُسمَح للعملية بالاستمرار عبر الدالة next
أما إذا كانت النتيجة صفرًا، فهذا يعني أن المستخدم ليس لديه الأذونات الكافية، وبالتالي ترجع الرمز HTTP 403.
الآن، نحتاج إلى إضافة البرمجية الوسيطة الخاصة بالاستيثاق في وجهة وحدة المستخدم الموجودة في الملف
/users/routes.config.js
، بحيث نضيف التحقق من صلاحيات المستخدم قبل السماح له بالوصول إلى العمليات المختلفة:
app.post('/users', [ UsersController.insert ]); app.get('/users', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(PAID), UsersController.list ]); app.get('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.getById ]); app.patch('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.patchById ]); app.delete('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(ADMIN), UsersController.removeById ]);
هكذا نكون أكملنا التطوير الأساسي لواجهة REST API باستخدام Node.js وكل ما يتبقى هو اختبارها للتأكد من خلوها من الأخطاء.
تشغيل واختبار الواجهة البرمجية باستخدام Insomnia
سنختبر الواجهة البرمجية باستخدام Insomnia، وهو برنامج عميل REST مناسب يحتوي على نسخة مجانية، ومن الأفضل بالتأكيد تضمين اختبارات الكود وإنشاء تقارير للأخطاء بشكل مناسب داخل المشروع، لكن Insomnia يعد خيارًا جيدًا حاليًا لمجرد اختبار الواجهة البرمجية.
لنتمكن من إنشاء مستخدم، نحتاج فقط إلى إرسال طلب POST
مع البيانات المطلوبة لنقطة الوصول المناسبة.
بعد نجاح الطلب، سنحفظ المعرّف الذي أنشأته لنا الواجهة البرمجية لهذا المستخدم، لاستخدامه لاحقًا في عمليات لاحقة مثل التحديث أو الحذف.
يمكننا الآن توليد رمز JWT باستخدام نقطة الوصول /auth/
:
يجب أن نحصل بنتيجة الطلب على رمز وصول Token
كما يلي:
الآن، سننسخ هذا الرمز ونضيف قبله كلمة Bearer
، مع التأكد من ترك فراغ بين الكلمتين. وسنضيف هذا الرمز لكافة ترويسات الطلب Request Headers ضمن الحقل Authorization
.
تعرض الصورة التالية استجابة الواجهة البرمجية API عند طلب بيانات مستخدم معين من نقطة الوصول /users/:userId
بعد إضافة البرمجية الوسيطة للتحقق من الصلاحيات Permissions Middleware، مع وجود رمز مصادقة صالح Valid Token.
كما ذكرنا سابقًا نعرض هنا كلمة المرور لتبسيط الشرح، ولكن لا ننسى أنه لا يجب نهائيًا عرض كلمة المرور في الاستجابات، سواءً كانت مشفرة أم لم تكن.
لنحاول الآن الحصول على قائمة بجميع المستخدمين بإرسال الطلب GET
على النحو التالي:
سنتفاجأ بالحصول على استجابة برمز الخطأ 403:
السبب وراء عدم القدرة على الوصول إلى هذه النقطة هو أن مستوى الصلاحيات الحالي للمستخدم هو permissionLevel=1
وهذا لا يمنحه الإذن الكافي للوصول إلى البيانات المطلوبة، لذلك يجب تحديث الصلاحيات permissionLevel
لهذا المستخدم من القيمة 1 للقيمة 7، أو ربما للقيمة 5 فهي كافية للوصول إلى نقطة الوصول الحالية.
يمكننا القيام بهذا التحديث عبر واجهة MongoDB التفاعلية يدويًا، مع تعديل المعرف ليتناسب مع المستخدم الحالي لدينا كما يلي:
db.users.update({"_id" : ObjectId("5b02c5c84817bf28049e58a3")},{$set:{"permissionLevel":5}})
سنحتاج بعد ذلك لتوليد رمز JWT جديد. وبعدها سنحصل على استجابة صحيحة للطلب GET
:
سنختبر الآن وظيفة تحديث بيانات المستخدم عبر إرسال طلب POST
مع بعض الحقول إلى نقطة الوصول /users/:userId
:
نتوقع الحصول على استجابة بالرمز 204 للتأكد بأن العملية نجحت، ولكن يمكننا التأكد عبر طلب معلومات المستخدم مجددًا.
وأخيرا سنحتاج لاختبار حذف المستخدم، للقيام بهذا يجب علينا إنشاء مستخدم جديد، مع عدم نسيان الاحتفاظ برمز المعرف الخاص بالمستخدم والتأكد من امتلاكنا لرمز JWT مناسب لمستخدم مسؤول. يجب علينا تعيين الرقم 2053 إلى userPermission
الخاص بالمستخدم الجديد والذي يساوي الرقم 2048، أي ADMIN
، بالإضافة إلى الرقم 5 الذي عيناه سابقًا ليتمكن من تنفيذ عملية حذف المستخدم بعد الانتهاء من ذلك وتوليد رمز JWT جديد، يجب علينا تحديث ترويسة طلب Authorization
الذي أضفناه سابقًا:
بعد إرسال طلب DELETE
لنقطة الوصول /users/:userId
يفترض أن نحصل على استجابة برمز الحالة 204 لتأكيد نجاح العملية. ويمكننا التأكد مجددا عبر إرسال طلب /users/
لجميع المستخدمين الموجودين من خادم Node API الخاص بنا.
الخاتمة
تعلمنا في هذا المقال أهم الخطوات اللازمة لإنشاء واجهة برمجة تطبيقات REST API بسيطة وآمنة باستخدام Node.js باستخدام مجموعة من الأدوات والتقنيات، ويجب الانتباه إلى أننا تخطينا هنا اتباع أفضل الممارسات للسهولة ويجب الانتباه للأمور التالية عند تطوير واجهة برمجية فعلية:
- إضافة تحققات صحيحة وشاملة، كالتأكد من كون البريد الإلكتروني فريد لكل مستخدم
- إضافة اختبار الوحدة unit testing وتقارير للأخطاء
- منع المستخدمين من تغيير مستوى الصلاحيات الخاص بهم
- منع المسؤولين من حذف أنفسهم
- منع الكشف عن المعلومات الحساسة، ككلمات المرور المشفرة على سبيل المثال
-
نقل رمز JWT من ملف
common/config/env.config.js
إلى نظام مخصص لتوزيع الرموز السرية خارج الكود البرمجي، بحيث لا يعتمد على البيئة المحلية
ترجمة -وبتصرف- لمقال Creating a Secure REST API in Node.js لصاحبه Marcos Henrique da Silva.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.