Εξασφάλιση Node.js RESTful API με JSON Web Tokens

Αναρωτηθήκατε ποτέ πώς λειτουργεί ο έλεγχος ταυτότητας; Τι κρύβεται πίσω από την πολυπλοκότητα και τις αφαιρέσεις. Στην πραγματικότητα, τίποτα το ιδιαίτερο. Είναι ένας τρόπος κρυπτογράφησης μιας τιμής, με τη σειρά του δημιουργώντας ένα μοναδικό διακριτικό που χρησιμοποιούν οι χρήστες ως αναγνωριστικό. Αυτό το διακριτικό επαληθεύει την ταυτότητά σας. Μπορεί να πιστοποιήσει την ταυτότητά σας και να εξουσιοδοτήσει διάφορους πόρους στους οποίους έχετε πρόσβαση. Εάν κατά τύχη δεν γνωρίζετε καμία από αυτές τις λέξεις-κλειδιά, κάντε υπομονή, θα σας εξηγήσω τα παρακάτω.

Αυτό θα είναι ένας βήμα προς βήμα οδηγός για το πώς να προσθέσετε έλεγχο ταυτότητας βάσει διακριτικών σε ένα υπάρχον REST API. Η εν λόγω στρατηγική ελέγχου ταυτότητας είναι JWT (JSON Web Token). Εάν αυτό δεν σας πει πολλά, είναι εντάξει. Ήταν εξίσου περίεργο για μένα όταν άκουσα για πρώτη φορά τον όρο.

Τι σημαίνει στην πραγματικότητα η JWT από κάτω προς τη γη; Ας αναλύσουμε τι αναφέρει ο επίσημος ορισμός:

Το JSON Web Token (JWT) είναι ένα συμπαγές μέσο, ​​ασφαλές για URL, που αντιπροσωπεύει αξιώσεις για μεταφορά μεταξύ δύο μερών. Οι αξιώσεις σε ένα JWT κωδικοποιούνται ως αντικείμενο JSON που χρησιμοποιείται ως ωφέλιμο φορτίο μιας δομής JSON Web Signature (JWS) ή ως απλού κειμένου μιας δομής JSON Web Encryption (JWE), επιτρέποντας στις αξιώσεις να υπογράφονται ψηφιακά ή να προστατεύονται από ακεραιότητα με κωδικό ελέγχου ταυτότητας μηνυμάτων (MAC) ή / και κρυπτογραφημένο.

- Task Force μηχανικής Διαδικτύου (IETF)

Αυτό ήταν ένα μπουκιά. Ας το μεταφράσουμε στα Αγγλικά. Το JWT είναι μια κωδικοποιημένη συμβολοσειρά χαρακτήρων που είναι ασφαλές για αποστολή μεταξύ δύο υπολογιστών εάν και οι δύο έχουν HTTPS. Το διακριτικό αντιπροσωπεύει μια τιμή που είναι προσβάσιμη μόνο από τον υπολογιστή που έχει πρόσβαση στο μυστικό κλειδί με το οποίο κρυπτογραφήθηκε. Αρκετά απλό, σωστά;

Πώς μοιάζει αυτό στην πραγματική ζωή; Ας υποθέσουμε ότι ένας χρήστης θέλει να συνδεθεί στον λογαριασμό του. Στέλνουν ένα αίτημα με τα απαιτούμενα διαπιστευτήρια, όπως email και κωδικό πρόσβασης στον διακομιστή. Ο διακομιστής ελέγχει αν τα διαπιστευτήρια είναι έγκυρα. Εάν είναι, ο διακομιστής δημιουργεί ένα διακριτικό χρησιμοποιώντας το επιθυμητό ωφέλιμο φορτίο και ένα μυστικό κλειδί. Αυτή η σειρά χαρακτήρων που προκύπτει από την κρυπτογράφηση ονομάζεται διακριτικό. Στη συνέχεια, ο διακομιστής το στέλνει πίσω στον πελάτη. Ο πελάτης, με τη σειρά του, αποθηκεύει το διακριτικό για να το χρησιμοποιήσει σε κάθε άλλο αίτημα που θα στείλει ο χρήστης. Η πρακτική της προσθήκης ενός διακριτικού στις κεφαλίδες αιτήματος είναι ως τρόπος εξουσιοδότησης του χρήστη για πρόσβαση σε πόρους. Αυτό είναι ένα πρακτικό παράδειγμα του τρόπου λειτουργίας του JWT.

Εντάξει, είναι αρκετή συζήτηση! Το υπόλοιπο αυτού του σεμιναρίου θα είναι κωδικοποίηση, και θα ήθελα πολύ αν ακολουθήσετε και να κωδικοποιήσετε μαζί μου, καθώς προχωράμε. Κάθε απόσπασμα κώδικα ακολουθείται από μια εξήγηση. Πιστεύω ότι ο καλύτερος τρόπος να το κατανοήσετε σωστά θα είναι να τον κωδικοποιήσετε μόνοι σας.

Πριν ξεκινήσω, υπάρχουν μερικά πράγματα που πρέπει να γνωρίζετε για το Node.js και ορισμένα πρότυπα EcmaScript που θα χρησιμοποιώ. Δεν θα χρησιμοποιώ το ES6, καθώς δεν είναι τόσο αρχάριος όσο το παραδοσιακό JavaScript. Όμως, θα περιμένω να γνωρίζετε ήδη πώς να δημιουργήσετε ένα RESTful API με το Node.js. Εάν όχι, μπορείτε να κάνετε παράκαμψη και να το ελέγξετε πριν προχωρήσετε.

Επίσης, ολόκληρο το demo βρίσκεται στο 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 διασφαλίζει ότι ο διακομιστής μας περιστρέφεται.

Προχωρήστε και εγκαταστήστε όλες τις απαιτούμενες ενότητες Node. Επιστρέψτε στο παράθυρο του τερματικού σας. Βεβαιωθείτε ότι βρίσκεστε στο φάκελο με το όνομα " nodejs-restful-api " και εκτελέστε το npm install. Περιμένετε ένα ή δύο δευτερόλεπτα για την εγκατάσταση των ενοτήτων. Τώρα θα πρέπει να προσθέσετε μια συμβολοσειρά σύνδεσης βάσης δεδομένων σε db.js .

Μεταβείτε στο mLab, δημιουργήστε έναν λογαριασμό εάν δεν έχετε ήδη και ανοίξτε τον πίνακα ελέγχου της βάσης δεδομένων σας. Δημιουργήστε μια νέα βάση δεδομένων, ονομάστε την όπως θέλετε και προχωρήστε στη σελίδα διαμόρφωσης. Προσθέστε έναν χρήστη βάσης δεδομένων στη βάση δεδομένων σας και αντιγράψτε τη συμβολοσειρά σύνδεσης από τον πίνακα ελέγχου στον κωδικό σας.

Το μόνο που πρέπει να κάνετε τώρα είναι να αλλάξετε τις τιμές placeholder για και . Αντικαταστήστε τα με το όνομα χρήστη και τον κωδικό πρόσβασης του χρήστη που δημιουργήσατε για τη βάση δεδομένων. Μια λεπτομερή βήμα προς βήμα εξήγηση αυτής της διαδικασίας μπορεί να βρεθεί στον οδηγό που συνδέεται παραπάνω.

Ας υποθέσουμε ότι ο χρήστης που δημιούργησα για τη βάση δεδομένων ονομάζεται 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 Web Token.

Αποποίηση ευθυνών : Έχετε υπόψη σας, σε καμία περίπτωση δεν θα πρέπει ποτέ, (ΠΟΤΕ!) Να έχετε το μυστικό κλειδί σας δημόσια ορατό έτσι. Να τοποθετείτε πάντα όλα τα κλειδιά σας σε μεταβλητές περιβάλλοντος! Το γράφω έτσι μόνο για σκοπούς επίδειξης.

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

Με αυτήν την προσθήκη είστε έτοιμοι να αρχίσετε να προσθέτετε τη λογική ελέγχου ταυτότητας. Δημιουργήστε ένα φάκελο με όνομα auth και ξεκινήστε προσθέτοντας ένα αρχείο με το όνομα 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

Αυτά είναι όλα τα modules που χρειαζόμαστε για να εφαρμόσουμε τον επιθυμητό έλεγχο ταυτότητας. Τώρα είστε έτοιμοι να δημιουργήσετε ένα /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 }); }); });

Here we’re expecting the user to send us three values, a name, an email and a password. We’re immediately going to take the password and encrypt it with Bcrypt’s hashing method. Then take the hashed password, include name and email and create a new user. After the user has been successfully created, we’re at ease to create a token for that user.

The jwt.sign() method takes a payload and the secret key defined in config.js as parameters. It creates a unique string of characters representing the payload. In our case, the payload is an object containing only the id of the user. Let’s write a piece of code to get the user id based on the token we got back from the register endpoint.

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); });
Οι λειτουργίες του Middleware είναι συναρτήσεις που έχουν πρόσβαση στο αντικείμενο αιτήματος ( req), στο αντικείμενο απόκρισης ( res) και στη nextλειτουργία στον κύκλο αιτήματος-απόκρισης της εφαρμογής. Η nextσυνάρτηση είναι μια συνάρτηση στον δρομολογητή Express που, όταν καλείται, εκτελεί το μεσαίο λογισμικό που διαδέχεται το τρέχον μεσαίο λογισμικό.

- Χρησιμοποιώντας το middleware, 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.

Οι λειτουργίες του Middleware χρησιμοποιούνται ως γέφυρες ανάμεσα σε ορισμένα κομμάτια κώδικα. Όταν χρησιμοποιούνται στην αλυσίδα λειτουργίας ενός τελικού σημείου, μπορούν να είναι εξαιρετικά χρήσιμα για την εξουσιοδότηση και τον χειρισμό σφαλμάτων.

Ελπίζω εσείς παιδιά και κορίτσια να σας άρεσαν να το διαβάζω όσο μου άρεσε να το γράφω. Μέχρι την επόμενη φορά, να είστε περίεργοι και να διασκεδάσετε.

Πιστεύετε ότι αυτό το σεμινάριο θα βοηθήσει κάποιον; Μην διστάσετε να μοιραστείτε. Αν σας άρεσε, παρακαλώ χειροκρότημα για μένα.