Les interfaces de programmation d'application (API) sont partout. Ils permettent aux logiciels de communiquer avec d'autres logiciels - internes ou externes - de manière cohérente, ce qui est un ingrédient clé de l'évolutivité, sans parler de la réutilisation.
Il est assez courant de nos jours pour les services en ligne d'avoir des API publiques. Ceux-ci permettent à d'autres développeurs d'intégrer facilement des fonctionnalités telles que les connexions aux réseaux sociaux, les paiements par carte de crédit et le suivi des comportements. La de facto la norme qu'ils utilisent pour cela s'appelle REpresentational State Transfer (REST).
Alors qu'une multitude de plates-formes et de langages de programmation peuvent être utilisés pour la tâche, par exemple, ASP.NET Core , Laravel (PHP) , ou Bouteille (Python) - dans ce didacticiel, nous allons créer un backend d'API REST basique mais sécurisé à l'aide de la pile suivante:
Les développeurs qui suivent ce didacticiel doivent également être à l'aise avec le terminal (ou l'invite de commande).
Remarque: Nous ne couvrirons pas ici une base de code frontale, mais le fait que notre back-end soit écrit en JavaScript facilite le partage de code (des modèles objet, par exemple) dans toute la pile.
Les API REST sont utilisées pour accéder aux données et les manipuler à l'aide d'un ensemble commun d'opérations sans état. Ces opérations font partie intégrante du protocole HTTP et représentent les fonctionnalités essentielles de création, de lecture, de mise à jour et de suppression (CRUD), mais pas de manière propre:
POST
(créer une ressource ou fournir généralement des données)GET
(récupérer un index des ressources ou une ressource individuelle)PUT
(créer ou remplacer une ressource)PATCH
(mettre à jour / modifier une ressource)DELETE
(supprimer une ressource)En utilisant ces opérations HTTP et un nom de ressource comme adresse, nous pouvons créer une API REST en créant un point de terminaison pour chaque opération. Et en implémentant le modèle, nous aurons une base stable et facilement compréhensible nous permettant de faire évoluer le code rapidement et de le maintenir par la suite. Comme mentionné précédemment, la même base sera utilisée pour intégrer des fonctionnalités tierces, dont la plupart utilisent également des API REST, ce qui accélère cette intégration.
Pour l'instant, commençons à créer notre API REST sécurisée à l'aide de Node.js!
Dans ce tutoriel, nous allons créer une API REST assez courante (et très pratique) pour une ressource appelée users
.
Notre ressource aura la structure de base suivante:
id
(un UUID généré automatiquement)firstName
lastName
email
password
permissionLevel
(qu'est-ce que cet utilisateur est autorisé à faire?)Et nous allons créer les opérations suivantes pour cette ressource:
POST
sur le noeud final /users
(créer un nouvel utilisateur)GET
sur le noeud final /users
(lister tous les utilisateurs)GET
sur le noeud final /users/:userId
(obtenir un utilisateur spécifique)PATCH
sur le noeud final /users/:userId
(mettre à jour les données pour un utilisateur spécifique)DELETE
sur le noeud final /users/:userId
(supprimer un utilisateur spécifique)Nous utiliserons également des jetons Web JSON (JWT) pour les jetons d'accès. À cette fin, nous allons créer une autre ressource appelée auth
qui attendra l'e-mail et le mot de passe d'un utilisateur et, en retour, générera le jeton utilisé pour l'authentification sur certaines opérations. (Excellent article de Dejan Milosevic sur JWT pour des applications REST sécurisées en Java va plus en détail à ce sujet; Les principes sont les mêmes.)
Tout d'abord, assurez-vous que la dernière version de Node.js est installée. Pour cet article, j'utiliserai la version 14.9.0; cela peut également fonctionner sur des versions plus anciennes.
Ensuite, assurez-vous que vous avez MongoDB installée. Nous n'expliquerons pas les spécificités de Mongoose et MongoDB qui sont utilisées ici, mais pour faire fonctionner les bases, démarrez simplement le serveur en mode interactif (c'est-à-dire à partir de la ligne de commande en tant que mongo
) plutôt qu'en tant que service. En effet, à un moment donné de ce didacticiel, nous devrons interagir avec MongoDB directement plutôt que via notre code Node.js.
Remarque: avec MongoDB, il n'est pas nécessaire de créer une base de données spécifique comme cela pourrait être le cas dans certains scénarios de SGBDR. Le premier appel d'insertion de notre code Node.js déclenchera sa création automatiquement.
Ce didacticiel ne contient pas tout le code nécessaire à un projet de travail. Il est plutôt prévu que vous cloniez le repo compagnon et suivez simplement les mises en évidence au fur et à mesure que vous lisez, mais vous pouvez également copier des fichiers et des extraits spécifiques du référentiel si nécessaire, si vous préférez.
Accédez au résultat rest-api-tutorial/
dossier dans votre terminal. Vous verrez que notre projet contient trois dossiers de modules:
common
(gestion de tous les services partagés et informations partagées entre les modules utilisateur)users
(tout ce qui concerne les utilisateurs)auth
(gestion de la génération JWT et du flux de connexion)Maintenant, exécutez npm install
(ou yarn
si vous l'avez.)
Félicitations, vous disposez maintenant de toutes les dépendances et de la configuration nécessaires pour exécuter notre simple back-end API REST.
Nous utiliserons Mangouste , une bibliothèque de modélisation de données d'objets (ODM) pour MongoDB, pour créer le modèle utilisateur dans le schéma utilisateur.
Tout d'abord, nous devons créer le schéma Mongoose dans /users/models/users.model.js
:
const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, permissionLevel: Number });
Une fois que nous avons défini le schéma, nous pouvons facilement attacher le schéma au modèle utilisateur.
const userModel = mongoose.model('Users', userSchema);
Après cela, nous pouvons utiliser ce modèle pour implémenter toutes les opérations CRUD que nous voulons dans nos points de terminaison Express.
comment obtenir l'url m3u8
Commençons par l'opération 'créer un utilisateur' en définissant l'itinéraire dans users/routes.config.js
:
app.post('/users', [ UsersController.insert ]);
Ceci est tiré dans notre application Express dans le principal index.js
fichier. Le UsersController
objet est importé de notre contrôleur, où nous hachons le mot de passe de manière appropriée, défini dans /users/controllers/users.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}); }); };
À ce stade, nous pouvons tester notre modèle Mongoose en exécutant le serveur (npm start
) et en envoyant un POST
demande à /users
avec quelques données JSON:
{ 'firstName' : 'Marcos', 'lastName' : 'Silva', 'email' : ' [email protected] ', 'password' : 's3cr3tp4sswo4rd' }
Il existe plusieurs outils que vous pouvez utiliser pour cela. Insomnia (couvert ci-dessous) et Postman sont des outils d'interface graphique populaires, et curl
est un choix CLI courant. Vous pouvez même simplement utiliser JavaScript, par exemple depuis la console des outils de développement intégrés de votre navigateur:
fetch('http://localhost:3600/users', { method: 'POST', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ 'firstName': 'Marcos', 'lastName': 'Silva', 'email': ' [email protected] ', '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); });
À ce stade, le résultat d'une publication valide sera simplement l'identifiant de l'utilisateur créé: { 'id': '5b02c5c84817bf28049e58a3' }
. Nous devons également ajouter le createUser
méthode au modèle dans users/models/users.model.js
:
exports.createUser = (userData) => { const user = new User(userData); return user.save(); };
Tout est prêt, nous devons maintenant voir si l'utilisateur existe. Pour cela, nous allons implémenter la fonctionnalité «get user by id» pour le terminal suivant: users/:userId
.
Tout d'abord, nous créons une route dans /users/routes/config.js
:
app.get('/users/:userId', [ UsersController.getById ]);
Ensuite, nous créons le contrôleur dans /users/controllers/users.controller.js
:
exports.getById = (req, res) => { UserModel.findById(req.params.userId).then((result) => { res.status(200).send(result); }); };
Et enfin, ajoutez le findById
méthode au modèle dans /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; }); };
La réponse sera comme ceci:
{ 'firstName': 'Marcos', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==', 'permissionLevel': 1, 'id': '5b02c5c84817bf28049e58a3' }
Notez que nous pouvons voir le mot de passe haché. Pour ce tutoriel, nous montrons le mot de passe, mais la meilleure pratique évidente est de ne jamais révéler le mot de passe, même s'il a été haché. Une autre chose que nous pouvons voir est le permissionLevel
, que nous utiliserons pour gérer les autorisations utilisateur plus tard.
En répétant le modèle présenté ci-dessus, nous pouvons maintenant ajouter la fonctionnalité pour mettre à jour l'utilisateur. Nous utiliserons le PATCH
opération puisqu'elle nous permettra d'envoyer uniquement les champs que nous voulons modifier. L'itinéraire sera donc PATCH
à /users/:userid
, et nous enverrons tous les champs que nous souhaitons modifier. Nous devrons également implémenter une validation supplémentaire car les modifications doivent être limitées à l'utilisateur en question ou à un administrateur, et seul un administrateur doit être en mesure de modifier le permissionLevel
. Nous allons ignorer cela pour le moment et y revenir une fois que nous aurons implémenté le module d'authentification. Pour l'instant, notre contrôleur ressemblera à ceci:
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({}); }); };
Par défaut, nous enverrons un code HTTP 204 sans corps de réponse pour indiquer que la demande a réussi.
Et nous devrons ajouter le patchUser
méthode au modèle:
exports.patchUser = (id, userData) => { return User.findOneAndUpdate({ _id: id }, userData); };
La liste des utilisateurs sera implémentée sous la forme GET
à /users/
par le responsable du traitement suivant:
exports.list = (req, res) => { let limit = req.query.limit && req.query.limit { res.status(200).send(result); }) };
La méthode du modèle correspondante sera:
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); } }) }); };
La réponse de liste résultante aura la structure suivante:
[ { 'firstName': 'Marco', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==', 'permissionLevel': 1, 'id': '5b02c5c84817bf28049e58a3' }, { 'firstName': 'Paulo', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==', 'permissionLevel': 1, 'id': '5b02d038b653603d1ca69729' } ]
Et la dernière partie à implémenter est le DELETE
à /users/:userId
.
Notre contrôleur de suppression sera:
exports.removeById = (req, res) => { UserModel.removeById(req.params.userId) .then((result)=>{ res.status(204).send({}); }); };
Comme précédemment, le contrôleur renverra le code HTTP 204 et aucun corps de contenu comme confirmation.
La méthode de modèle correspondante devrait ressembler à ceci:
exports.removeById = (userId) => { return new Promise((resolve, reject) => { User.deleteMany({_id: userId}, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); };
Nous avons maintenant toutes les opérations nécessaires pour manipuler la ressource utilisateur, et nous en avons terminé avec le contrôleur utilisateur. L'idée principale de ce code est de vous donner les concepts de base de l'utilisation du modèle REST. Nous devrons revenir à ce code pour mettre en œuvre certaines validations et autorisations, mais d'abord, nous devrons commencer à renforcer notre sécurité. Créons le module d'authentification.
Avant de pouvoir sécuriser le users
en mettant en œuvre le middleware d'autorisation et de validation, nous devrons être en mesure de générer un jeton valide pour l'utilisateur actuel. Nous générerons un JWT en réponse à l'utilisateur fournissant un e-mail et un mot de passe valides. JWT est un jeton Web JSON remarquable que vous pouvez utiliser pour que l'utilisateur fasse plusieurs demandes en toute sécurité sans valider à plusieurs reprises. Il a généralement une heure d'expiration et un nouveau jeton est recréé toutes les quelques minutes pour assurer la sécurité de la communication. Pour ce didacticiel, cependant, nous renoncerons à actualiser le jeton et resterons simples avec un seul jeton par connexion.
Tout d'abord, nous allons créer un point de terminaison pour POST
demandes à /auth
Ressource. Le corps de la demande contiendra l'adresse e-mail et le mot de passe de l'utilisateur:
{ 'email' : ' [email protected] ', 'password' : 's3cr3tp4sswo4rd2' }
Avant d'engager le contrôleur, nous devons valider l'utilisateur dans /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']}); } } }); };
Cela fait, nous pouvons passer au contrôleur et générer le 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}); } };
Même si nous n'actualiserons pas le jeton dans ce didacticiel, le contrôleur a été configuré pour permettre une telle génération afin de faciliter sa mise en œuvre dans le développement ultérieur.
Il ne nous reste plus qu'à créer la route et à appeler le middleware approprié dans /authorization/routes.config.js
:
app.post('/auth', [ VerifyUserMiddleware.hasAuthValidFields, VerifyUserMiddleware.isPasswordAndUserMatch, AuthorizationController.login ]);
La réponse contiendra le JWT généré dans le champ accessToken:
{ 'accessToken': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY', 'refreshToken': 'U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ==' }
Après avoir créé le jeton, nous pouvons l'utiliser dans le Authorization
en-tête sous la forme Bearer ACCESS_TOKEN
.
La première chose à définir est de savoir qui peut utiliser le users
Ressource. Voici les scénarios que nous devrons gérer:
Après avoir identifié ces scénarios, nous aurons d'abord besoin d'un middleware qui valide toujours l'utilisateur s'il utilise un JWT valide. Le middleware dans /common/middlewares/auth.validation.middleware.js
peut être aussi simple que:
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(); } };
Nous utiliserons les codes d'erreur HTTP pour gérer les erreurs de demande:
Nous pouvons utiliser l'opérateur AND bit à bit (bitmasking) pour contrôler les permissions. Si nous définissons chaque autorisation requise comme une puissance de 2, nous pouvons traiter chaque bit de l'entier 32 bits comme une autorisation unique. Un administrateur peut alors avoir toutes les autorisations en définissant sa valeur d'autorisation sur 2147483647. Cet utilisateur pourrait alors avoir accès à n'importe quel itinéraire. Comme autre exemple, un utilisateur dont la valeur d'autorisation était définie sur 7 aurait des autorisations sur les rôles marqués par des bits pour les valeurs 1, 2 et 4 (deux à la puissance 0, 1 et 2).
Le middleware pour cela ressemblerait à ceci:
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(); } }; };
Le middleware est générique. Si le niveau d'autorisation utilisateur et le niveau d'autorisation requis coïncident sur au moins un bit, le résultat sera supérieur à zéro et nous pouvons laisser l'action se poursuivre; sinon, le code HTTP 403 sera renvoyé.
Maintenant, nous devons ajouter le middleware d'authentification aux routes du module de l'utilisateur dans /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 ]);
Ceci conclut le développement de base de notre API REST. Tout ce qui reste à faire est de tout tester.
Insomnie est un client REST décent avec une bonne version gratuite. La meilleure pratique consiste, bien sûr, à inclure des tests de code et à implémenter des rapports d'erreur appropriés dans le projet, mais les clients REST tiers sont parfaits pour tester et mettre en œuvre des solutions tierces lorsque le rapport d'erreur et le débogage du service ne sont pas disponibles. Nous allons l'utiliser ici pour jouer le rôle d'une application et avoir un aperçu de ce qui se passe avec notre API.
Pour créer un utilisateur, il suffit de POST
les champs obligatoires vers le point final approprié et stockent l'ID généré pour une utilisation ultérieure.
L'API répondra avec l'ID utilisateur:
Nous pouvons maintenant générer le JWT en utilisant le /auth/
point final:
Nous devrions obtenir un jeton en guise de réponse:
Prenez le accessToken
, préfixez-le avec Bearer
(souvenez-vous de l'espace), et ajoutez-le aux en-têtes de requête sous Authorization
:
Si nous ne le faisons pas maintenant que nous avons implémenté le middleware des autorisations, toute demande autre que l'enregistrement renverrait le code HTTP 401. Avec le jeton valide en place, cependant, nous obtenons la réponse suivante de /users/:userId
:
De plus, comme mentionné précédemment, nous affichons tous les champs, à des fins pédagogiques et par souci de simplicité. Le mot de passe (haché ou autre) ne doit jamais être visible dans la réponse.
Essayons d'obtenir une liste d'utilisateurs:
Surprise! Nous obtenons une réponse 403.
Notre utilisateur n'a pas les autorisations pour accéder à ce point de terminaison. Nous devrons changer le permissionLevel
de notre utilisateur de 1 à 7 (ou même 5 feraient l'affaire, puisque nos niveaux d'autorisations gratuites et payantes sont représentés respectivement par 1 et 4.) Nous pouvons le faire manuellement dans MongoDB, à son invite interactive, comme ceci (avec l'ID modifié par votre résultat local):
db.users.update({'_id' : ObjectId('5b02c5c84817bf28049e58a3')},{$set:{'permissionLevel':5}})
Ensuite, nous devons générer un nouveau JWT.
Après cela, nous obtenons la réponse appropriée:
Ensuite, testons la fonctionnalité de mise à jour en envoyant un PATCH
demande avec quelques champs à notre /users/:userId
point final:
Nous attendons une réponse 204 comme confirmation d'une opération réussie, mais nous pouvons demander à nouveau à l'utilisateur de vérifier.
Enfin, nous devons supprimer l'utilisateur. Nous devrons créer un nouvel utilisateur comme décrit ci-dessus (n'oubliez pas de noter l'ID utilisateur) et nous assurer que nous avons le JWT approprié pour un utilisateur administrateur. Le nouvel utilisateur aura besoin de ses autorisations définies sur 2053 (soit 2048— ADMIN
—plus nos 5 précédentes) pour pouvoir également effectuer l'opération de suppression. Une fois cela fait et un nouveau JWT généré, nous devrons mettre à jour notre Authorization
en-tête de la demande:
Envoi d'un DELETE
demande à /users/:userId
, nous devrions obtenir une réponse 204 comme confirmation. Nous pouvons, encore une fois, vérifier en demandant /users/
pour lister tous les utilisateurs existants.
exécuter sur le fil d'interface utilisateur android
Avec les outils et méthodes abordés dans ce didacticiel, vous devriez maintenant être en mesure de créer des API REST simples et sécurisées sur Node.js. Un grand nombre de bonnes pratiques qui ne sont pas essentielles au processus ont été ignorées, alors n'oubliez pas de:
common/config/env.config.js
à un hors-repo, non basé sur l'environnement mécanisme de distribution secret Un dernier exercice pour le lecteur peut être de convertir la base de code de son utilisation des promesses JavaScript en asynchroniser / attendre technique.
Pour ceux d'entre vous qui pourraient être intéressés, il y a maintenant aussi une version TypeScript du projet disponible.
En relation: 5 choses que vous n'avez jamais faites avec une spécification REST