تأمين واجهات برمجة تطبيقات Node.js RESTful مع رموز الويب JSON

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

سيكون هذا برنامجًا تعليميًا خطوة بخطوة حول كيفية إضافة المصادقة القائمة على الرمز المميز إلى واجهة برمجة تطبيقات REST الحالية. استراتيجية المصادقة المعنية هي JWT (JSON Web Token). إذا لم يخبرك ذلك كثيرًا ، فلا بأس. كان الأمر غريبًا بالنسبة لي عندما سمعت هذا المصطلح لأول مرة.

ماذا تعني JWT في الواقع من وجهة نظر واقعية؟ لنفصل ما ينص عليه التعريف الرسمي:

JSON Web Token (JWT) هي وسيلة مضغوطة وآمنة لعناوين URL لتمثيل المطالبات التي سيتم نقلها بين طرفين. يتم ترميز المطالبات في JWT ككائن JSON يتم استخدامه كحمل لهيكل JSON Web Signature (JWS) أو كنص عادي لبنية تشفير الويب JSON (JWE) ، مما يتيح توقيع المطالبات رقميًا أو حماية تكاملها باستخدام رمز مصادقة الرسائل (MAC) و / أو المشفر.

- فريق هندسة الإنترنت (IETF)

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

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

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

قبل أن أبدأ ، هناك بعض الأشياء التي تحتاج إلى معرفتها حول Node.js وبعض معايير EcmaScript التي سأستخدمها. لن أستخدم ES6 ، لأنها ليست صديقة للمبتدئين مثل JavaScript التقليدي. ولكن ، أتوقع أنك تعرف بالفعل كيفية إنشاء RESTful API باستخدام Node.js. إذا لم يكن الأمر كذلك ، فيمكنك الالتفاف والتحقق من ذلك قبل المتابعة.

أيضًا ، العرض التوضيحي بأكمله موجود على GitHub إذا كنت ترغب في رؤيته بالكامل.

لنبدأ في كتابة بعض التعليمات البرمجية ، أليس كذلك؟

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

git clone //github.com/adnanrahic/nodejs-restful-api.git

سترى مجلد يظهر ، افتحه. دعنا نلقي نظرة على هيكل المجلد.

> user - User.js - UserController.js - db.js - server.js - app.js - package.json

لدينا مجلد مستخدم بنموذج ووحدة تحكم ، وقد تم تنفيذ CRUD الأساسي بالفعل. يحتوي app.js الخاص بنا على التكوين الأساسي. و db.js يتأكد يربط تطبيق لقاعدة البيانات. و server.js يتأكد لنا خادم يدور فوق.

انطلق وقم بتثبيت جميع وحدات العقدة المطلوبة. عد إلى النافذة الطرفية. تأكد من أنك في المجلد المسمى " nodejs-restful-api " وقم بتشغيل npm install. انتظر ثانية أو ثانيتين حتى يتم تثبيت الوحدات. الآن تحتاج إلى إضافة سلسلة اتصال قاعدة البيانات في db.js .

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

كل ما عليك فعله الآن هو تغيير قيم العناصر النائبة لـ و . استبدلها باسم المستخدم وكلمة المرور للمستخدم الذي أنشأته لقاعدة البيانات. يمكن العثور على شرح مفصل خطوة بخطوة لهذه العملية في البرنامج التعليمي المرتبط أعلاه.

لنفترض أن المستخدم الذي أنشأته لقاعدة البيانات تمت تسميته wallyبكلمة مرور theflashisawesome. مع وضع ذلك في الاعتبار ، يجب أن يبدو ملف db.js الآن على النحو التالي :

var mongoose = require('mongoose'); mongoose.connect('mongodb://wally:[email protected]:47072/securing-rest-apis-with-jwt', { useMongoClient: true });

انطلق وقم بتدوير الخادم ، مرة أخرى في نوع النافذة الطرفية node server.js. يجب أن ترى Express server listening on port 3000تسجيل الدخول إلى المحطة.

أخيرًا ، بعض التعليمات البرمجية.

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

ثانيًا ، نريد إضافة إذن. إجراء منح المستخدمين الإذن للوصول إلى موارد معينة على REST API.

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

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

// config.js module.exports = { 'secret': 'supersecret' };

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

أضف هذا الجزء من التعليمات البرمجية إلى الجزء العلوي من AuthController.js .

// AuthController.js var express = require('express'); var router = express.Router(); var bodyParser = require('body-parser'); router.use(bodyParser.urlencoded({ extended: false })); router.use(bodyParser.json()); var User = require('../user/User');

أنت الآن جاهز لإضافة الوحدات النمطية لاستخدام JSON Web Tokens وتشفير كلمات المرور. الصق هذه الشفرة في AuthController.js :

var jwt = require('jsonwebtoken'); var bcrypt = require('bcryptjs'); var config = require('../config');

افتح نافذة طرفية في مجلد مشروعك وقم بتثبيت الوحدات التالية:

npm install jsonwebtoken --save npm install bcryptjs --save

هذه هي كل الوحدات التي نحتاجها لتنفيذ المصادقة المطلوبة. أنت الآن جاهز لإنشاء /registerنقطة نهاية. أضف هذا الجزء من الكود إلى AuthController.js الخاص بك :

router.post('/register', function(req, res) { var hashedPassword = bcrypt.hashSync(req.body.password, 8); User.create({ name : req.body.name, email : req.body.email, password : hashedPassword }, function (err, user) { if (err) return res.status(500).send("There was a problem registering the user.") // create a token var token = jwt.sign({ id: user._id }, config.secret, { expiresIn: 86400 // expires in 24 hours }); res.status(200).send({ auth: true, token: token }); }); });

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

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

router.get('/me', function(req, res) { var token = req.headers['x-access-token']; if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' }); jwt.verify(token, config.secret, function(err, decoded) { if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' }); res.status(200).send(decoded); }); });

Here we’re expecting the token be sent along with the request in the headers. The default name for a token in the headers of an HTTP request is x-access-token. If there is no token provided with the request the server sends back an error. To be more precise, an 401 unauthorized status with a response message of No token provided. If the token exists, the jwt.verify() method will be called. This method decodes the token making it possible to view the original payload. We’ll handle errors if there are any and if there are not, send back the decoded value as the response.

Finally we need to add the route to the AuthController.js in our main app.js file. First export the router from AuthController.js:

// add this to the bottom of AuthController.js module.exports = router;

Then add a reference to the controller in the main app, right above where you exported the app.

// app.js var AuthController = require('./auth/AuthController'); app.use('/api/auth', AuthController); module.exports = app;

Let’s test this out. Why not?

Open up your REST API testing tool of choice, I use Postman or Insomnia, but any will do.

Go back to your terminal and run node server.js. If it is running, stop it, save all changes to you files, and run node server.js again.

Open up Postman and hit the register endpoint (/api/auth/register). Make sure to pick the POST method and x-www-form-url-encoded. Now, add some values. My user’s name is Mike and his password is ‘thisisasecretpassword’. That’s not the best password I’ve ever seen, to be honest, but it’ll do. Hit send!

See the response? The token is a long jumbled string. To try out the /api/auth/me endpoint, first copy the token. Change the URL to /me instead of /register, and the method to GET. Now you can add the token to the request header.

Voilà! The token has been decoded into an object with an id field. Want to make sure that the id really belongs to Mike, the user we just created? Sure you do. Jump back into your code editor.

// in AuthController.js change this line res.status(200).send(decoded); // to User.findById(decoded.id, function (err, user) { if (err) return res.status(500).send("There was a problem finding the user."); if (!user) return res.status(404).send("No user found."); res.status(200).send(user); });

Now when you send a request to the /me endpoint you’ll see:

The response now contains the whole user object! Cool! But, not good. The password should never be returned with the other data about the user. Let’s fix this. We can add a projection to the query and omit the password. Like this:

User.findById(decoded.id, { password: 0 }, // projection function (err, user) { if (err) return res.status(500).send("There was a problem finding the user."); if (!user) return res.status(404).send("No user found."); res.status(200).send(user); });

That’s better, now we can see all values except the password. Mike’s looking good.

Did someone say login?

After implementing the registration, we should create a way for existing users to log in. Let’s think about it for a second. The register endpoint required us to create a user, hash a password, and issue a token. What will the login endpoint need us to implement? It should check if a user with the given email exists at all. But also check if the provided password matches the hashed password in the database. Only then will we want to issue a token. Add this to your AuthController.js.

router.post('/login', function(req, res) { User.findOne({ email: req.body.email }, function (err, user) { if (err) return res.status(500).send('Error on the server.'); if (!user) return res.status(404).send('No user found.'); var passwordIsValid = bcrypt.compareSync(req.body.password, user.password); if (!passwordIsValid) return res.status(401).send({ auth: false, token: null }); var token = jwt.sign({ id: user._id }, config.secret, { expiresIn: 86400 // expires in 24 hours }); res.status(200).send({ auth: true, token: token }); }); });

First of all we check if the user exists. Then using Bcrypt’s .compareSync() method we compare the password sent with the request to the password in the database. If they match we .sign() a token. That’s pretty much it. Let’s try it out.

Cool it works! What if we get the password wrong?

Great, when the password is wrong the server sends a response status of 401 unauthorized. Just what we wanted!

To finish off this part of the tutorial, let’s add a simple logout endpoint to nullify the token.

// AuthController.js router.get('/logout', function(req, res) { res.status(200).send({ auth: false, token: null }); });

Disclaimer: The logout endpoint is not needed. The act of logging out can solely be done through the client side. A token is usually kept in a cookie or the browser’s localstorage. Logging out is as simple as destroying the token on the client. This /logout endpoint is created to logically depict what happens when you log out. The token gets set to null.

With this we’ve finished the authentication part of the tutorial. Want to move on to the authorization? I bet you do.

Do you have permission to be here?

To comprehend the logic behind an authorization strategy we need to wrap our head around something called middleware. Its name is self explanatory, to some extent, isn’t it? Middleware is a piece of code, a function in Node.js, that acts as a bridge between some parts of your code.

When a request reaches an endpoint, the router has an option to pass the request on to the next middleware function in line. Emphasis on the word next! Because that’s exactly what the name of the function is! Let’s see an example. Comment out the line where you send back the user as a response. Add a next(user) right underneath.

router.get('/me', function(req, res, next) { var token = req.headers['x-access-token']; if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' }); jwt.verify(token, config.secret, function(err, decoded) { if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' }); User.findById(decoded.id, { password: 0 }, // projection function (err, user) { if (err) return res.status(500).send("There was a problem finding the user."); if (!user) return res.status(404).send("No user found."); // res.status(200).send(user); Comment this out! next(user); // add this line }); }); }); // add the middleware function router.use(function (user, req, res, next) { res.status(200).send(user); });
وظائف البرامج الوسيطة هي وظائف لها حق الوصول إلى كائن الطلب ( req) وكائن الاستجابة ( res) nextوالوظيفة في دورة الطلب والاستجابة للتطبيق. و nextالوظيفة هي وظيفة في جهاز التوجيه اكسبرس التي عند استدعاء، ينفذ الوسيطة خلفا الوسيطة الحالية.

- استخدام البرامج الوسيطة ، expressjs.com

انتقل مرة أخرى إلى ساعي البريد وتحقق مما يحدث عندما تصل إلى /api/auth/meنقطة النهاية. هل يفاجئك أن النتيجة هي نفسها بالضبط؟ يجب أن يكون!

إخلاء المسؤولية : تابع واحذف هذه العينة قبل المتابعة حيث إنها تستخدم فقط لإثبات منطق الاستخدام next().

Let’s take this same logic and apply it to create a middleware function to check the validity of tokens. Create a new file in the auth folder and name it VerifyToken.js. Paste this snippet of code in there.

var jwt = require('jsonwebtoken'); var config = require('../config'); function verifyToken(req, res, next) { var token = req.headers['x-access-token']; if (!token) return res.status(403).send({ auth: false, message: 'No token provided.' }); jwt.verify(token, config.secret, function(err, decoded) { if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' }); // if everything good, save to request for use in other routes req.userId = decoded.id; next(); }); } module.exports = verifyToken;

Let’s break it down. We’re going to use this function as a custom middleware to check if a token exists and whether it is valid. After validating it, we add the decoded.id value to the request (req) variable. We now have access to it in the next function in line in the request-response cycle. Calling next() will make sure flow will continue to the next function waiting in line. In the end, we export the function.

Now, open up the AuthController.js once again. Add a reference to VerifyToken.js at the top of the file and edit the /me endpoint. It should now look like this:

// AuthController.js var VerifyToken = require('./VerifyToken'); // ... router.get('/me', VerifyToken, function(req, res, next) { User.findById(req.userId, { password: 0 }, function (err, user) { if (err) return res.status(500).send("There was a problem finding the user."); if (!user) return res.status(404).send("No user found."); res.status(200).send(user); }); }); // ...

See how we added VerifyToken in the chain of functions? We now handle all the authorization in the middleware. This frees up all the space in the callback to only handle the logic we need. This is an awesome example of how to write DRY code. Now, every time you need to authorize a user you can add this middleware function to the chain. Test it in Postman again, to make sure it still works like it should.

Feel free to mess with the token and try the endpoint again. With an invalid token, you’ll see the desired error message, and be sure the code you wrote works the way you want.

Why is this so powerful? You can now add the VerifyTokenmiddleware to any chain of functions and be sure the endpoints are secured. Only users with verified tokens can access the resources!

Wrapping your head around everything.

Don’t feel bad if you did not grasp everything at once. Some of these concepts are hard to understand. It’s fine to take a step back and rest your brain before trying again. That’s why I recommend you go through the code by yourself and try your best to get it to work.

Again, here’s the GitHub repository. You can catch up on any things you may have missed, or just get a better look at the code if you get stuck.

Remember, authentication is the act of logging a user in. Authorization is the act of verifying the access rights of a user to interact with a resource.

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

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

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