Atelier 07
Voici la structure de répertoires de notre application Node.js Express :
– config
configure MySQL database & Sequelize
configure Auth Key
– routes
auth.routes.js: POST signup & signin
user.routes.js: GET public & protected resources
– middlewares
verifySignUp.js: check duplicate Username or Email
authJwt.js: verify Token, check User roles in database
– controllers
auth.controller.js: handle signup & signin actions
1
user.controller.js: return public & protected content
– models for Sequelize Models
user.model.js
role.model.js
– server.js: import and initialize necessary modules and routes, listen for connections.
Créer une application Node.js
Tout d'abord, nous créons un dossier pour notre projet :
$ mkdir node-js-jwt-auth
$ cd node-js-jwt-auth
Ensuite, nous initialisons l'application Node.js avec un fichier package.json :
npm init
name: (node-js-jwt-auth)
version: (1.0.0)
description: Node.js Demo for JWT Authentication
entry point: (index.js) server.js
test command:
git repository:
keywords: node.js, express, jwt, authentication, mysql
author: bezkoder
license: (ISC)
Is this ok? (yes) yes
2
Nous devons installer les modules nécessaires : express, cors, sequelize, et . Exécutez la
commande :mysql2jsonwebtokenbcryptjs:
npm install express sequelize mysql2 cors jsonwebtoken bcryptjs --save
Le fichier package.json ressemble maintenant à ceci :
{
"name": "node-js-jwt-auth",
"version": "1.0.0",
"description": "Node.js Demo for JWT Authentication",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"node.js",
"jwt",
"authentication",
"express",
"mysql"
],
"author": "bezkoder",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mysql2": "^2.3.3",
"sequelize": "^6.32.1"
}
}
Configurer le serveur Web Express
Dans le dossier racine, créons un nouveau fichier server.js :
const express = require("express");
const cors = require("cors");
3
const app = express();
var corsOptions = {
origin: "http://localhost:8081"
};
app.use(cors(corsOptions));
// parse requests of content-type - application/json
app.use(express.json());
// parse requests of content-type - application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));
// simple route
app.get("/", (req, res) => {
res.json({ message: "Welcome to bezkoder application." });
});
// set port, listen for requests
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}.`);
});
Laissez-moi vous expliquer ce que nous venons de faire :
– import expresset corsmodules :
Express sert à construire les API Rest
cors fournit un middleware Express pour activer CORS
– créez une application Express, puis ajoutez un analyseur de corps de requête et corsdes
middlewares à l’aide app.use()de la méthode. Notez que nous définissons
origin: http://localhost:8081.
– définir une route GET simple à tester.
– écoutez sur le port 8080 les demandes entrantes.
Lançons maintenant l'application avec la commande : node server.js.
Ouvrez votre navigateur avec l'url http://localhost:8080/ , vous verrez :
4
Configure MySQL database & Sequelize
In the app folder, create config folder for configuration with db.config.js file like this:
module.exports = {
HOST: "localhost",
USER: "root",
PASSWORD: "123456",
DB: "testdb",
dialect: "mysql",
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
};
Les cinq premiers paramètres concernent la connexion MySQL.
poolest facultatif, il sera utilisé pour la configuration du pool de connexions Sequelize :
max: nombre maximum de connexion dans le pool
min: nombre minimum de connexion dans le pool
idle: durée maximale, en millisecondes, pendant laquelle une connexion peut être
inactive avant d'être libérée
acquire : durée maximale, en millisecondes, pendant laquelle ce pool tentera
d'établir une connexion avant de générer une erreur
Définir le modèle Sequelize
Dans le dossier models , créez Userun Rolemodèle de données comme le code suivant :
models/user.model.js
module.exports = (sequelize, Sequelize) => {
const User = sequelize.define("users", {
username: {
type: Sequelize.STRING
5
},
email: {
type: Sequelize.STRING
},
password: {
type: Sequelize.STRING
}
});
return User;
};
models/role.model.js
module.exports = (sequelize, Sequelize) => {
const Role = sequelize.define("roles", {
id: {
type: Sequelize.INTEGER,
primaryKey: true
},
name: {
type: Sequelize.STRING
}
});
return Role;
};
Ces modèles Sequelize représentent la table des utilisateurs et des rôles dans la base de
données MySQL.
Après avoir initialisé Sequelize, nous n'avons pas besoin d'écrire des fonctions CRUD,
Sequelize les prend toutes en charge :
créez un nouvel utilisateur :create(object)
trouver un utilisateur par identifiant :findByPk(id)
trouver un utilisateur par email :findOne({ where: { email: ... } })
obtenir tous les utilisateurs :findAll()
trouver tous les utilisateurs par nom d'utilisateur :findAll({ where:
{ username: ... } })
Ces fonctions seront utilisées dans nos contrôleurs et middlewares.
6
Initialiser Sequelize
Créez maintenant app / models / index.js avec un contenu comme celui-ci :
const config = require("../config/db.config.js");
const Sequelize = require("sequelize");
const sequelize = new Sequelize(
config.DB,
config.USER,
config.PASSWORD,
{
host: config.HOST,
dialect: config.dialect,
pool: {
max: config.pool.max,
min: config.pool.min,
acquire: config.pool.acquire,
idle: config.pool.idle
}
}
);
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
db.user = require("../models/user.model.js")(sequelize, Sequelize);
db.role = require("../models/role.model.js")(sequelize, Sequelize);
db.role.belongsToMany(db.user, {
through: "user_roles"
});
db.user.belongsToMany(db.role, {
through: "user_roles"
});
db.ROLES = ["user", "admin", "moderator"];
7
module.exports = db;
L'association entre les utilisateurs et les rôles est une relation plusieurs-à-plusieurs :
– Un utilisateur peut avoir plusieurs rôles.
– Un rôle peut être assumé par plusieurs utilisateurs.
Nous utilisons User.belongsToMany(Role)pour indiquer que le modèle utilisateur peut
appartenir à plusieurs Role s et vice versa.
Avec through, nous allons avoir une nouvelle table user_roles comme connexion entre les
utilisateurs et la table des rôles via leur clé primaire comme clés étrangères.
N'oubliez pas d'appeler sync()la méthode dans server.js .
...
const app = express();
app.use(...);
const db = require("./app/models");
const Role = db.role;
db.sequelize.sync({force: true}).then(() => {
console.log('Drop and Resync Db');
initial();
});
...
function initial() {
Role.create({
id: 1,
name: "user"
});
Role.create({
id: 2,
name: "moderator"
});
Role.create({
id: 3,
name: "admin"
});
8
}
initial()La fonction nous aide à créer 3 lignes dans la base de données.
En développement, vous devrez peut-être supprimer les tables existantes et resynchroniser
la base de données. Vous pouvez donc utiliser force: truele code ci-dessus.
Pour la production, insérez simplement ces lignes manuellement et utilisez- sync()les sans
paramètres pour éviter de perdre des données :
...
const app = express();
app.use(...);
const db = require("./app/models");
db.sequelize.sync();
...
Configurer la clé d'authentification
jsonwebtoken fonctionne comme verify()ou sign()utilise un algorithme qui nécessite
une clé secrète (sous forme de chaîne) pour encoder et décoder le jeton.
Dans le dossier app / config , créez le fichier auth.config.js avec le code suivant :
module.exports = {
secret: "bezkoder-secret-key"
};
Vous pouvez créer votre propre secretchaîne.
Créer des fonctions Middleware
Pour vérifier une action d'inscription, nous avons besoin de 2 fonctions :
– vérifier si usernameou emailest en double ou non
– vérifier si rolesla demande existe ou non
middleware/verifySignUp.js
const db = require("../models");
const ROLES = db.ROLES;
const User = db.user;
checkDuplicateUsernameOrEmail = (req, res, next) => {
// Username
User.findOne({
where: {
username: req.body.username
9
}
}).then(user => {
if (user) {
res.status(400).send({
message: "Failed! Username is already in use!"
});
return;
}
// Email
User.findOne({
where: {
email: req.body.email
}
}).then(user => {
if (user) {
res.status(400).send({
message: "Failed! Email is already in use!"
});
return;
}
next();
});
});
};
checkRolesExisted = (req, res, next) => {
if (req.body.roles) {
for (let i = 0; i < req.body.roles.length; i++) {
if (!ROLES.includes(req.body.roles[i])) {
res.status(400).send({
message: "Failed! Role does not exist = " + req.body.roles[i]
});
return;
}
}
}
10
next();
};
const verifySignUp = {
checkDuplicateUsernameOrEmail: checkDuplicateUsernameOrEmail,
checkRolesExisted: checkRolesExisted
};
module.exports = verifySignUp;
Pour traiter l'authentification et l'autorisation, nous disposons de ces fonctions :
- vérifier si cela tokenest fourni, légal ou non. Nous obtenons le jeton du x-access-token des
en-têtes HTTP, puis utilisons la fonction de jsonwebtokenverify() .
- Vérifiez si rolesl'utilisateur contient le rôle requis ou non. middleware/authJwt.js
const jwt = require("jsonwebtoken");
const config = require("../config/auth.config.js");
const db = require("../models");
const User = db.user;
verifyToken = (req, res, next) => {
let token = req.headers["x-access-token"];
if (!token) {
return res.status(403).send({
message: "No token provided!"
});
}
jwt.verify(token,
config.secret,
(err, decoded) => {
if (err) {
return res.status(401).send({
message: "Unauthorized!",
});
}
req.userId = decoded.id;
11
next();
});
};
isAdmin = (req, res, next) => {
User.findByPk(req.userId).then(user => {
user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) {
if (roles[i].name === "admin") {
next();
return;
}
}
res.status(403).send({
message: "Require Admin Role!"
});
return;
});
});
};
isModerator = (req, res, next) => {
User.findByPk(req.userId).then(user => {
user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) {
if (roles[i].name === "moderator") {
next();
return;
}
}
res.status(403).send({
message: "Require Moderator Role!"
});
});
});
};
12
isModeratorOrAdmin = (req, res, next) => {
User.findByPk(req.userId).then(user => {
user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) {
if (roles[i].name === "moderator") {
next();
return;
}
if (roles[i].name === "admin") {
next();
return;
}
}
res.status(403).send({
message: "Require Moderator or Admin Role!"
});
});
});
};
const authJwt = {
verifyToken: verifyToken,
isAdmin: isAdmin,
isModerator: isModerator,
isModeratorOrAdmin: isModeratorOrAdmin
};
module.exports = authJwt;
middleware/index.js
const authJwt = require("./authJwt");
const verifySignUp = require("./verifySignUp");
module.exports = {
authJwt,
verifySignUp
};
13
Créer des contrôleurs
Contrôleur pour l'authentification
Il existe 2 fonctions principales pour l'authentification :
-signup : créer un nouvel utilisateur dans la base de données (le rôle est l'utilisateur si le
rôle n'est pas spécifié)
-signin :
retrouver usernamela requête dans la base de données, si elle existe
comparer passwordavec passworddans la base de données en utilisant bcrypt , si
c'est correct
générer un jeton en utilisant jsonwebtoken
renvoyer les informations utilisateur et accéder au jeton
controllers/auth.controller.js
const db = require("../models");
const config = require("../config/auth.config");
const User = db.user;
const Role = db.role;
const Op = db.Sequelize.Op;
var jwt = require("jsonwebtoken");
var bcrypt = require("bcryptjs");
exports.signup = (req, res) => {
// Save User to Database
User.create({
username: req.body.username,
email: req.body.email,
password: bcrypt.hashSync(req.body.password, 8)
})
.then(user => {
if (req.body.roles) {
Role.findAll({
where: {
name: {
[Op.or]: req.body.roles
}
}
14
}).then(roles => {
user.setRoles(roles).then(() => {
res.send({ message: "User was registered successfully!" });
});
});
} else {
// user role = 1
user.setRoles([1]).then(() => {
res.send({ message: "User was registered successfully!" });
});
}
})
.catch(err => {
res.status(500).send({ message: err.message });
});
};
exports.signin = (req, res) => {
User.findOne({
where: {
username: req.body.username
}
})
.then(user => {
if (!user) {
return res.status(404).send({ message: "User Not found." });
}
var passwordIsValid = bcrypt.compareSync(
req.body.password,
user.password
);
if (!passwordIsValid) {
return res.status(401).send({
accessToken: null,
message: "Invalid Password!"
});
15
}
const token = jwt.sign({ id: user.id },
config.secret,
{
algorithm: 'HS256',
allowInsecureKeySizes: true,
expiresIn: 86400, // 24 hours
});
var authorities = [];
user.getRoles().then(roles => {
for (let i = 0; i < roles.length; i++) {
authorities.push("ROLE_" + roles[i].name.toUpperCase());
}
res.status(200).send({
id: user.id,
username: user.username,
email: user.email,
roles: authorities,
accessToken: token
});
});
})
.catch(err => {
res.status(500).send({ message: err.message });
});
};
Contrôleur pour les tests Autorisation
Il existe 4 fonctions :
– /api/test/allpour l'accès public
– /api/test/userpour les utilisateurs connectés
(rôle : utilisateur / modérateur / administrateur )
– /api/test/modpour les utilisateurs ayant le rôle de modérateur
– /api/test/adminpour les utilisateurs ayant le rôle d'administrateur
controllers/user.controller.js
exports.allAccess = (req, res) => {
res.status(200).send("Public Content.");
16
};
exports.userBoard = (req, res) => {
res.status(200).send("User Content.");
};
exports.adminBoard = (req, res) => {
res.status(200).send("Admin Content.");
};
exports.moderatorBoard = (req, res) => {
res.status(200).send("Moderator Content.");
};
Définir des itinéraires
Lorsqu'un client envoie une requête pour un point de terminaison à l'aide d'une requête
HTTP (GET, POST, PUT, DELETE), nous devons déterminer comment le serveur répondra en
configurant les routes.
Nous pouvons séparer nos itinéraires en 2 parties : pour l'authentification et pour
l'autorisation (accès aux ressources protégées).
Authentification:
POSTE/api/auth/signup
POSTE/api/auth/signin
routes/auth.routes.js
const { verifySignUp } = require("../middleware");
const controller = require("../controllers/auth.controller");
module.exports = function(app) {
app.use(function(req, res, next) {
res.header(
"Access-Control-Allow-Headers",
"x-access-token, Origin, Content-Type, Accept"
);
next();
});
17
app.post(
"/api/auth/signup",
[
verifySignUp.checkDuplicateUsernameOrEmail,
verifySignUp.checkRolesExisted
],
controller.signup
);
app.post("/api/auth/signin", controller.signin);
};
Autorisation:
OBTENIR/api/test/all
GET /api/test/userpour les utilisateurs connectés
(utilisateur/modérateur/administrateur)
GET /api/test/modpour le modérateur
OBTENIR /api/test/adminpour l'administrateur
routes/user.routes.js
const { authJwt } = require("../middleware");
const controller = require("../controllers/user.controller");
module.exports = function(app) {
app.use(function(req, res, next) {
res.header(
"Access-Control-Allow-Headers",
"x-access-token, Origin, Content-Type, Accept"
);
next();
});
app.get("/api/test/all", controller.allAccess);
app.get(
"/api/test/user",
[authJwt.verifyToken],
controller.userBoard
);
18
app.get(
"/api/test/mod",
[authJwt.verifyToken, authJwt.isModerator],
controller.moderatorBoard
);
app.get(
"/api/test/admin",
[authJwt.verifyToken, authJwt.isAdmin],
controller.adminBoard
);
};
N'oubliez pas d'ajouter ces routes dans server.js :
...
// routes
require('./app/routes/auth.routes')(app);
require('./app/routes/user.routes')(app);
// set port, listen for requests
...
Exécuter et tester avec les résultats
Exécutez l'application Node.js avec la commande :node server.js
Enregistrez certains utilisateurs avec /signupl'API :
administrateur avec adminrôle
mod avec moderatoret userrôles
zkoder avec userrôle
19
Accéder à la ressource publique : GET/api/test/all
Accéder à la ressource protégée : GET/api/test/user
20
Connectez-vous à un compte (avec un mauvais mot de passe) : POST/api/auth/signin
Connectez-vous à un compte : POST/api/auth/signin
21
Accéder aux ressources protégées : GET/api/test/user
22
23