Fermer

mars 30, 2020

Créer une application avec Angular et la CLI angulaire –37 minutes de lecture

ES6 en action: let et const


Pour les formations en ligne Angular dirigées par des experts, vous ne pouvez pas passer par Ultimate Angular par Todd Motto. Essayez ses cours ici et utilisez le code SITEPOINT pour obtenir une réduction de 25% et aider à prendre en charge SitePoint.

Dans ce didacticiel, nous allons examiner la gestion de l'authentification des utilisateurs dans la pile MEAN. Nous utiliserons l'architecture MEAN la plus courante consistant à avoir une application angulaire à page unique utilisant une API REST construite avec Node, Express et MongoDB.

Lorsque nous pensons à l'authentification des utilisateurs, nous devons aborder les choses suivantes:

  1. let un registre d'utilisateurs
  2. enregistre les données des utilisateurs, mais ne stocke jamais directement les mots de passe
  3. permet à un utilisateur qui revient de se connecter
  4. garde en vie la session d'un utilisateur connecté entre les visites de pages
  5. certaines pages ne peuvent être vues que par les journaux dans les utilisateurs
  6. changez la sortie à l'écran en fonction de l'état de connexion (par exemple, un bouton "connexion" ou un bouton "mon profil").

Avant de plonger dans le code, prenons quelques minutes pour examen de haut niveau du fonctionnement de l'authentification dans la pile MEAN.

Le flux d'authentification de la pile MEAN

À quoi ressemble l'authentification dans la pile MEAN?

Toujours en gardant cela à un niveau élevé, ces sont les composants du flux:

  • les données utilisateur sont stockées dans Mongo DB, avec les mots de passe hachés
  • Les fonctions CRUD sont intégrées dans une API Express – Créer (s'inscrire), Lire (se connecter, obtenir le profil), Mettre à jour, Supprimer
  • une application angulaire appelle l'API et traite les réponses [19659005] l'API Express génère un jeton Web JSON (JWT, prononcé "Jot") lors de l'inscription ou de la connexion, et le transmet à l'application angulaire
  • l'application angulaire stocke le JWT afin de maintenir l'utilisateur session
  • l'application Angular vérifie la validité du JWT lors de l'affichage des vues protégées
  • l'application Angular renvoie le JWT à Express lors de l'appel des routes d'API protégées.

Les JWT sont préférés aux cookies pour maintenir l'état de session dans le navigateur. Les cookies sont meilleurs pour maintenir l'état lors de l'utilisation d'une application côté serveur.

L'exemple d'application

Le code de ce tutoriel est disponible sur GitHub . Pour exécuter l'application, vous devez avoir installé Node.js ainsi que MongoDB . (Pour obtenir des instructions sur l'installation, reportez-vous à la documentation officielle de Mongo – Windows, Linux, macOS ).

L'application angulaire

Pour simplifier l'exemple de ce didacticiel, nous allons commencer avec une application angulaire de quatre pages:

  1. page d'accueil
  2. page d'enregistrement
  3. page de connexion
  4. page de profil

Les pages sont assez simples et ressemblent à ceci pour commencer:

 Captures d'écran de l'application

La page de profil ne sera accessible qu'aux utilisateurs authentifiés. Tous les fichiers de l'application Angular se trouvent dans un dossier à l'intérieur de l'application Angular CLI appelé / client .

Nous utiliserons l'Angular CLI pour créer et exécuter le serveur local. Si vous ne connaissez pas la CLI angulaire, reportez-vous au didacticiel Création d'une application Todo avec la CLI angulaire pour commencer.

L'API REST

Nous allons également commencer avec le squelette de une API REST construite avec Node, Express et MongoDB, utilisant Mongoose pour gérer les schémas. Cette API devrait initialement avoir trois routes:

  1. / api / register (POST), pour gérer les nouveaux utilisateurs qui s'inscrivent
  2. / api / login (POST), pour gérer le retour utilisateurs se connectant
  3. / api / profile / USERID (GET), pour renvoyer les détails du profil lorsqu'ils reçoivent un USERID

Configurons cela maintenant. Nous pouvons utiliser l'outil de générateur express pour créer une grande partie de la plaque de la chaudière pour nous. Si c'est nouveau pour vous, nous avons un tutoriel sur son utilisation ici .

Installez-le avec npm i -g express-generator . Ensuite, créez une nouvelle application Express, en choisissant Pug comme moteur de vue:

 express -v pug mean-authentication

Une fois le générateur exécuté, accédez au répertoire du projet et installez les dépendances:

 cd mean-authentication
npm i

Au moment de la rédaction de cet article, cela contient une version obsolète de Pug. Corrigeons cela:

 npm i pug @ latest

Nous pouvons également installer Mongoose pendant que nous y sommes:

 npm i mongoose

Ensuite, nous devons créer notre structure de dossiers.

  • Supprimer le dossier public : rm -rf public .
  • Créer une api répertoire: mkdir api .
  • Créez un contrôleurs un modèles et un routes répertoire dans le api répertoire: mkdir -p api / {contrôleurs, modèles, itinéraires} .
  • . Créez un fichier authenication.js et un fichier profile.js . dans le répertoire controllers : touch api / controllers / {authentication.js, profile.js} .
  • Créez un fichier db.js et un fichier users.js dans le répertoire models : touch api / models / {db.js, users.js} .
  • Créer un index Fichier .js dans le répertoire routes : touch api / routes / i ndex.js .

Lorsque vous avez terminé, les choses devraient ressembler à ceci:

.
└── api
    ├── contrôleurs
    │ ├── authentication.js
    │ └── profile.js
    ├── modèles
    │ ├── db.js
    │ └── users.js
    └── itinéraires
        └── index.js

Ajoutons maintenant la fonctionnalité API. Remplacez le code dans app.js par ce qui suit:

 require ('./ api / models / db');

const cookieParser = require ('cookie-parser');
const createError = require ('erreurs-http');
const express = require ('express');
const logger = require ('morgan');
const path = require ('path');

const routesApi = require ('./ api / routes / index');

const app = express ();

// afficher la configuration du moteur
app.set ('vues', path.join (__ dirname, 'vues'));
app.set («moteur de vue», «pug»);

app.use (logger ('dev'));
app.use (express.json ());
app.use (express.urlencoded ({extended: false}));
app.use (cookieParser ());
app.use (express.static (path.join (__ dirname, 'public')));

app.use ('/ api', routesApi);

// attrape 404 et transmet au gestionnaire d'erreurs
app.use ((req, res, next) => {
  suivant (createError (404));
});

// gestionnaire d'erreurs
app.use ((err, req, res, next) => {
  // définir les locaux, ne fournissant qu'une erreur de développement
  res.locals.message = err.message;
  res.locals.error = req.app.get ('env') === 'development'? err: {};

  // affiche la page d'erreur
  res.status (err.status || 500);
  res.render ('error');
});

module.exports = app;

Ajoutez ce qui suit à api / models / db.js :

 require ('./ users');
const mangouste = require ('mangouste');
const dbURI = 'mongodb: // localhost: 27017 / meanAuth';

mongoose.set ('useCreateIndex', true);
mongoose.connect (dbURI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

mongoose.connection.on ('connecté', () => {
  console.log (`Mongoose connecté à $ {dbURI}`);
});
mongoose.connection.on ('error', (err) => {
  console.log (`Erreur de connexion Mongoose: $ {err}`);
});
mongoose.connection.on ('déconnecté', () => {
  console.log ('Mongoose déconnecté');
});

Ajoutez ce qui suit à api / routes / index.js :

 const ctrlAuth = require ('../ controllers / authentication');
const ctrlProfile = require ('../ controllers / profile');

const express = require ('express');
const router = express.Router ();

// profil
router.get ('/ profile /: userid', ctrlProfile.profileRead);

// authentification
router.post ('/ register', ctrlAuth.register);
router.post ('/ login', ctrlAuth.login);

module.exports = routeur;

Ajoutez ce qui suit à api / controllers / profile.js :

 module.exports.profileRead = (req, res) => {
  console.log (`Lecture de l'ID de profil: $ {req.params.userid}`);
  statut res (200);
  res.json ({
    message: `Profil lu: $ {req.params.userid}`
  });
};

Ajoutez ce qui suit à api / controllers / authentication.js :

 module.exports.register = (req, res) => {
  console.log (`Enregistrement de l'utilisateur: $ {req.body.email}`);
  statut res (200);
  res.json ({
    message: `Utilisateur enregistré: $ {req.body.email}`
  });
};

module.exports.login = (req, res) => {
  console.log (`Connexion de l'utilisateur: $ {req.body.email}`);
  statut res (200);
  res.json ({
    message: `Utilisateur connecté: $ {req.body.email}`
  });
};

Assurez-vous que Mongo est en cours d'exécution et enfin, démarrez le serveur avec npm run start . Si tout est correctement configuré, vous devriez voir un message dans votre terminal indiquant que Mongoose est connecté à mongodb: // localhost: 27017 / meanAuth et vous devriez maintenant pouvoir faire des demandes et obtenir des réponses de , l'API. Vous pouvez le tester avec un outil tel que Postman .

Création du schéma de données MongoDB avec Mongoose

Ensuite, ajoutons un schéma à api / models / users.js . Il définit le besoin d'une adresse e-mail, d'un nom, d'un hachage et d'un sel. Le hachage et le sel seront utilisés au lieu d'enregistrer un mot de passe. L'e-mail est défini sur unique car nous allons l'utiliser pour les informations de connexion. Voici le schéma:

 const mongoose = require ('mongoose');

const userSchema = new mongoose.Schema ({
  email: {
    type: String,
    unique: vrai,
    requis: vrai
  },
  Nom: {
    type: String,
    requis: vrai
  },
  hachage: chaîne,
  sel: String
});

mongoose.model ('Utilisateur', userSchema);

Gérer le mot de passe sans l'enregistrer

L'enregistrement des mots de passe des utilisateurs est un gros non. Si un pirate obtient une copie de votre base de données, vous devez vous assurer qu'il ne peut pas l'utiliser pour se connecter aux comptes. C'est là qu'interviennent le hachage et le sel.

Le sel est une chaîne de caractères unique à chaque utilisateur. Le hachage est créé en combinant le mot de passe fourni par l'utilisateur et le sel, puis en appliquant un cryptage unidirectionnel. Comme le hachage ne peut pas être décrypté, la seule façon d'authentifier un utilisateur est de prendre le mot de passe, de le combiner avec le sel et de le crypter à nouveau. Si la sortie de ceci correspond au hachage, le mot de passe doit être correct.

Pour effectuer le paramétrage et la vérification du mot de passe, nous pouvons utiliser des méthodes de schéma Mongoose. Ce sont essentiellement des fonctions que vous ajoutez au schéma. Ils utiliseront tous les deux le module de cryptage Node.js .

En haut du fichier de modèle users.js il faut un crypto pour que nous puissions l'utiliser: [19659058] const crypto = require ('crypto');

Rien ne doit être installé, car la cryptographie est livrée avec Node. La crypto elle-même a plusieurs méthodes; nous sommes intéressés par randomBytes pour créer le sel aléatoire et pbkdf2Sync pour créer le hachage.

Définition du mot de passe

Pour enregistrer la référence au mot de passe, nous pouvons créer une nouvelle méthode appelée setPassword sur le schéma userSchema qui accepte un paramètre de mot de passe. La méthode utilisera ensuite crypto.randomBytes pour définir le sel et crypto.pbkdf2Sync pour définir le hachage:

 userSchema.methods.setPassword = function (password) {
  this.salt = crypto.randomBytes (16) .toString ('hex');
  this.hash = crypto
    .pbkdf2Sync (mot de passe, this.salt, 1000, 64, 'sha512')
    .toString ('hex');
};

Nous utiliserons cette méthode lors de la création d'un utilisateur. Au lieu d'enregistrer le mot de passe dans un chemin de mot de passe nous pourrons le passer à la fonction setPassword pour définir le sel et hash chemins dans le document utilisateur.

Vérification du mot de passe

La vérification du mot de passe est un processus similaire, mais nous avons déjà le sel du modèle Mongoose. Cette fois, nous voulons simplement crypter le sel et le mot de passe et voir si la sortie correspond au hachage stocké.

Ajoutez une autre nouvelle méthode au fichier modèle users.js appelée validPassword :

 userSchema.methods.validPassword = fonction (mot de passe) {
  hachage const = crypto
    .pbkdf2Sync (mot de passe, this.salt, 1000, 64, 'sha512')
    .toString ('hex');
  retourner this.hash === hachage;
};

Génération d'un jeton Web JSON (JWT)

Une autre chose que le modèle Mongoose doit pouvoir faire est de générer un JWT, afin que l'API puisse l'envoyer comme réponse. Une méthode Mongoose est idéale ici aussi, car elle signifie que nous pouvons conserver le code au même endroit et l'appeler chaque fois que cela est nécessaire. Nous devrons l'appeler lorsqu'un utilisateur s'inscrit et lorsqu'un utilisateur se connecte.

Pour créer le JWT, nous utiliserons un package appelé jsonwebtoken qui doit être installé dans l'application, alors lancez ceci sur la ligne de commande:

 npm i jsonwebtoken

Exigez ensuite cela dans le fichier modèle users.js :

 const jwt = require ('jsonwebtoken');

Ce module expose une méthode de signe que nous pouvons utiliser pour créer un JWT, en lui passant simplement les données que nous voulons inclure dans le jeton, plus un secret que l'algorithme de hachage utilisera. Les données doivent être envoyées en tant qu'objet JavaScript et inclure une date d'expiration dans une propriété exp .

Ajout d'une méthode generateJwt à userSchema afin de retourner un JWT ressemble à ceci:

 userSchema.methods.generateJwt = function () {
  const expiration = new Date ();
  expiration.setDate (expiration.getDate () + 7);

  return jwt.sign (
    {
      _id: this._id,
      email: this.email,
      nom: this.name,
      exp: parseInt (expiry.getTime () / 1000)
    },
    'MON SECRET'
  ); // NE GARDEZ PAS VOTRE SECRET DANS LE CODE!
};

Remarque: il est important que votre secret soit conservé en toute sécurité: seul le serveur d'origine doit savoir de quoi il s'agit. Il est préférable de définir le secret comme variable d'environnement et de ne pas l'avoir dans le code source, surtout si votre code est stocké quelque part dans le contrôle de version.

Et c'est tout ce que nous devons faire avec la base de données. [19659097] Configurer Passport pour gérer l'authentification express

Passport est un module Node qui simplifie le processus de gestion de l'authentification dans Express. Il fournit une passerelle commune pour travailler avec de nombreuses «stratégies» d'authentification différentes, telles que la connexion avec Facebook, Twitter ou Oauth. La stratégie que nous utiliserons est appelée «locale», car elle utilise un nom d'utilisateur et un mot de passe stockés localement.

Pour utiliser Passport, commencez par l'installer et la stratégie, en les enregistrant dans package.json : [19659041] npm i passeport passeport-local

Configurer Passport

Dans le dossier api créez un nouveau dossier config et créez-y un fichier appelé passport.js . C'est là que nous définissons la stratégie:

 mkdir -p api / config
touchez api / config / passport.js

Avant de définir la stratégie, ce fichier doit nécessiter Passport, la stratégie, Mongoose et le modèle User :

 const mongoose = require ('mongoose');
passeport const = exigé («passeport»);
const LocalStrategy = require ('passport-local'). Strategy;
const User = mongoose.model ('Utilisateur');

Pour une stratégie locale, il suffit essentiellement d'écrire une requête Mongoose sur le modèle User . Cette requête doit trouver un utilisateur avec l'adresse e-mail spécifiée, puis appeler la méthode validPassword pour voir si les hachages correspondent.

Il n'y a qu'une seule curiosité de Passport à traiter. En interne, la stratégie locale pour Passport attend deux éléments de données appelés nom d'utilisateur et mot de passe . Cependant, nous utilisons l'e-mail comme identifiant unique, et non nom d'utilisateur . Ceci peut être configuré dans un objet options avec une propriété usernameField dans la définition de stratégie. Après cela, nous passons à la requête Mongoose.

Donc, dans l'ensemble, la définition de la stratégie ressemblera à ceci:

 passport.use (
  nouveau LocalStrategy (
    {
      usernameField: 'email'
    },
    fonction (nom d'utilisateur, mot de passe, terminé) {
      User.findOne ({email: nom d'utilisateur}, fonction (err, utilisateur) {
        si (err) {
          retour fait (err);
        }
        // Renvoie si l'utilisateur n'est pas trouvé dans la base de données
        si (! utilisateur) {
          return done (null, false, {
            message: 'Utilisateur introuvable'
          });
        }
        // Retourne si le mot de passe est incorrect
        if (! user.validPassword (mot de passe)) {
          return done (null, false, {
            message: 'Le mot de passe est incorrect'
          });
        }
        // Si les informations d'identification sont correctes, renvoyez l'objet utilisateur
        retour effectué (null, utilisateur);
      });
    }
  )
);

Notez comment la méthode de schéma validPassword est appelée directement sur l'instance de l'utilisateur .

Maintenant, Passport doit simplement être ajouté à l'application. Donc, dans app.js nous devons exiger le module Passport, exiger la configuration Passport et initialiser Passport en tant que middleware. Le placement de tous ces éléments dans app.js est très important, car ils doivent s'insérer dans une certaine séquence.

Le module Passeport doit être requis en haut du fichier avec l'autre général requiert les instructions:

 const cookieParser = require ('cookie-parser');
const createError = require ('erreurs-http');
const express = require ('express');
const logger = require ('morgan');
passeport const = exigé («passeport»);
const path = require ('path');

La configuration doit être requise après le modèle est requis, car la configuration fait référence au modèle.

 require ('./ api / models / db');
exiger («./ api / config / passport»);

Enfin, Passport devrait être initialisé en tant que middleware Express juste avant l'ajout des routes API, car ces routes sont la première fois que Passport sera utilisé:

 app.use (passport.initialize ());
app.use ("/ api", routesApi);

Nous avons maintenant configuré le schéma et le passeport. Ensuite, il est temps de les utiliser dans les routes et les contrôleurs de l'API.

Configurer les points de terminaison de l'API

Avec l'API, nous avons deux choses à faire:

  1. rendre les contrôleurs fonctionnels
  2. sécurisés la route / api / profile afin que seuls les utilisateurs authentifiés puissent y accéder

Coder les contrôleurs API d'enregistrement et de connexion

Dans l'exemple d'application, les contrôleurs d'enregistrement et de connexion se trouvent dans / api /controllers/authentication.js. Pour que les contrôleurs fonctionnent, le fichier doit nécessiter Passport, Mongoose et le modèle utilisateur:

 const mongoose = require ('mongoose');
passeport const = exigé («passeport»);
const User = mongoose.model ('Utilisateur');

Le contrôleur d'API de registre

Le contrôleur de registre doit effectuer les opérations suivantes:

  1. prendre les données du formulaire soumis et créer une nouvelle instance de modèle Mongoose
  2. appeler la méthode setPassword que nous créé plus tôt pour ajouter le sel et le hachage à l'instance
  3. enregistrer l'instance en tant qu'enregistrement dans la base de données
  4. générer un JWT
  5. envoyer le JWT à l'intérieur de la réponse JSON

Dans le code, tout cela ressemble comme ça. Cela devrait remplacer la fonction factice du registre que nous avons codée plus tôt:

 module.exports.register = (req, res) => {
  const user = new User ();

  user.name = req.body.name;
  user.email = req.body.email;

  user.setPassword (req.body.password);

  user.save (() => {
    const token = user.generateJwt ();
    statut res (200);
    res.json ({
      jeton: jeton
    });
  });
};

Cela utilise les méthodes setPassword et generateJwt que nous avons créées dans la définition du schéma Mongoose. Voyez comment le fait d'avoir ce code dans le schéma rend ce contrôleur plus facile à lire et à comprendre.

N'oubliez pas qu'en réalité, ce code aurait un certain nombre d'interruptions d'erreur, validant les entrées de formulaire et interceptant les erreurs dans le enregistrer la fonction . Ils sont omis ici pour mettre en évidence les principales fonctionnalités du code, mais si vous souhaitez un rafraîchissement, consultez " Formulaires, téléchargements de fichiers et sécurité avec Node.js et Express ".

Le Login API Controller

Le contrôleur de connexion remet à peu près tout le contrôle à Passport, bien que vous puissiez (et devriez) ajouter au préalable une certaine validation pour vérifier que les champs requis ont été envoyés.

Pour que Passport fasse sa magie et s'exécute la stratégie définie dans la configuration, nous devons appeler la méthode authenticate comme indiqué ci-dessous. Cette méthode appellera un rappel avec trois paramètres possibles err utilisateur et info . Si l'utilisateur est défini, il peut être utilisé pour générer un JWT à renvoyer au navigateur. Cela devrait remplacer la méthode factice de connexion que nous avons définie précédemment:

 module.exports.login = (req, res) => {
  passport.authenticate ('local', (err, utilisateur, info) => {
    // Si Passport lance / intercepte une erreur
    si (err) {
      res.status (404) .json (err);
      revenir;
    }

    // Si un utilisateur est trouvé
    if (utilisateur) {
      const token = user.generateJwt ();
      statut res (200);
      res.json ({
        jeton: jeton
      });
    } autre {
      // Si l'utilisateur n'est pas trouvé
      res.status (401) .json (info);
    }
  }) (req, res);
};

Sécurisation d'une route API

La dernière chose à faire dans le back-end est de s'assurer que seuls les utilisateurs authentifiés peuvent accéder à la route / api / profile . La façon de valider une demande est de s'assurer que le JWT envoyé avec elle est authentique, en utilisant à nouveau le secret. C'est pourquoi vous devez le garder secret et ne pas le placer dans le code.

Configuration de l'authentification de l'itinéraire

Nous devons d'abord installer un middleware appelé express-jwt :

 npm i express-jwt

Ensuite, nous devons l'exiger et le configurer dans le fichier où les routes sont définies. Dans l'exemple d'application, il s'agit de /api/routes/index.js . La configuration consiste à lui révéler le secret et, éventuellement, le nom de la propriété à créer sur l'objet req qui contiendra le JWT. Nous pourrons utiliser cette propriété à l'intérieur du contrôleur associé à l'itinéraire. Le nom par défaut de la propriété est user mais il s'agit du nom d'une instance de notre modèle Mongoose User nous allons donc le définir sur payload sur éviter toute confusion:

 // api / routes / index.js

const jwt = require ('express-jwt');

const auth = jwt ({
  secret: 'MY_SECRET',
  userProperty: 'payload'
});

...

Encore une fois, ne gardez pas le secret dans le code!

Application de l'authentification de route

Pour appliquer ce middleware, faites simplement référence à la fonction au milieu de la route à protéger, comme ceci:

 router.get ('/ profile', auth, ctrlProfile.profileRead);

Notez que nous avons changé / profile /: userid en / profile car l'ID sera obtenu auprès du JWT.

Si quelqu'un essaie d'accéder à cette route maintenant sans JWT valide, le middleware générera une erreur. Pour vous assurer que notre API fonctionne correctement, interceptez cette erreur et renvoyez une réponse 401 en ajoutant ce qui suit dans la section des gestionnaires d'erreurs du fichier principal app.js :

 // intercepte 404 et transmet l'erreur gestionnaire
app.use ((req, res, next) => {...});

// Détecte les erreurs non autorisées
app.use ((err, req, res) => {
  if (err.name === 'UnauthorizedError') {
    statut res. (401);
    res.json ({message: `$ {err.name}: $ {err.message}`});
  }
});

À ce stade, vous pouvez essayer d'obtenir le point de terminaison / api / profile à l'aide d'un outil tel que Postman ou dans votre navigateur et vous devriez voir une réponse 401. [19659156] Utilisation de l'authentification de route

Dans cet exemple, nous voulons uniquement que les utilisateurs puissent afficher leurs propres profils, nous obtenons donc l'ID utilisateur du JWT et nous l'utilisons dans une requête Mongoose.

Le contrôleur de cette l'itinéraire se trouve dans /api/controllers/profile.js . Le contenu entier de ce fichier ressemble à ceci:

 const mongoose = require ('mongoose');
const User = mongoose.model ('Utilisateur');

module.exports.profileRead = (req, res) => {
  // Si aucun ID utilisateur n'existe dans le JWT, renvoyer un 401
  if (! req.payload._id) {
    res.status (401) .json ({
      message: 'UnauthorizedError: profil privé'
    });
  } autre {
    // Sinon, continuez
    User.findById (req.payload._id) .exec (fonction (err, utilisateur) {
      res.status (200) .json (utilisateur);
    });
  }
};

Naturellement, cela devrait être étoffé avec un peu plus de capture d'erreur – par exemple, si l'utilisateur n'est pas trouvé – mais cet extrait est gardé bref pour démontrer les points clés de l'approche.

Et c'est tout pour le back end. La base de données est configurée, nous avons des points de terminaison API pour l'enregistrement et la connexion qui génèrent et renvoient un JWT, ainsi qu'un itinéraire protégé.

Sur le front-end!

Initialiser l'application angulaire

Nous allons pour utiliser la CLI Angluar dans cette section, donc avant d'aller plus loin, assurez-vous qu'elle est installée globalement:

 npm install -g @ angular / cli

Ensuite, dans le répertoire racine du projet, exécutez:

 ng new client

? Souhaitez-vous ajouter un routage angulaire? Oui
? Quel format de feuille de style aimeriez-vous utiliser? CSS
...
✔ Packages installés avec succès.
    Git initialisé avec succès.

Cela génère un nouveau répertoire client avec un AppModule et AppRoutingModule . En répondant «Oui» à «Souhaitez-vous ajouter un routage angulaire», le AppRoutingModule est automatiquement créé et importé dans AppModule pour nous.

Parce que nous allons utiliser Formes angulaires et client HTTP d'Angular, nous devons importer les FormsModule et HttpClientModule d'Angular. Modifiez le contenu de client / src / app / app.module.ts comme suit:

 importez {BrowserModule} de "@ angular / platform-browser";
importer {NgModule} depuis "@ angular / core";

importer {AppRoutingModule} depuis "./app-routing.module";
importer {AppComponent} depuis "./app.component";
importer {FormsModule} depuis "@ angular / forms";
importer {HttpClientModule} depuis "@ angular / common / http";

@NgModule ({
  déclarations: [
    AppComponent
  ],
  importations: [BrowserModule, AppRoutingModule, FormsModule, HttpClientModule],
  fournisseurs: [],
  bootstrap: [AppComponent]
})
classe d'exportation AppModule {}

Créer un service d'authentification angulaire

La plupart du travail dans le frontal peut être placé dans un service angulaire, créant des méthodes pour gérer:

  • enregistrement du JWT dans le stockage local
  • lecture du JWT à partir du stockage local
  • suppression du JWT du stockage local
  • appel des points de terminaison de l'API d'enregistrement et de connexion
  • vérification si un utilisateur est actuellement connecté
  • obtention des détails de l'utilisateur connecté du JWT

Nous ' ll faudra créer un nouveau service appelé AuthenticationService . Avec la CLI, cela peut être fait en exécutant:

 $ cd client
$ ng générer une authentification de service
CRÉER src / app / authentication.service.spec.ts (397 octets)
CRÉER src / app / authentication.service.ts (143 octets)

Dans l'exemple d'application, cela se trouve dans le fichier /client/src/app/authentication.service.ts :

 import {Injectable} de "@ angular / core";

@Injectable ({
  providedIn: "root"
})
classe d'exportation AuthenticationService {
  constructeur () {}
}

Stockage local: enregistrement, lecture et suppression d'un JWT

Pour garder un utilisateur connecté entre les visites, nous utilisons localStorage dans le navigateur pour enregistrer le JWT. Une alternative consiste à utiliser sessionStorage qui ne conservera le jeton que pendant la session actuelle du navigateur.

Premièrement, nous voulons créer quelques interfaces pour gérer les types de données. Ceci est utile pour vérifier le type de notre application. Le profil renvoie un objet au format UserDetails et les points de terminaison de connexion et d'enregistrement s'attendent à un TokenPayload pendant la demande et renvoient un objet TokenResponse :

 interface d'exportation UserDetails {
  _id: chaîne;
  email: chaîne;
  nom: chaîne;
  exp: nombre;
  iat: nombre;
}

interface TokenResponse {
  jeton: chaîne;
}

interface d'exportation TokenPayload {
  email: chaîne;
  mot de passe: chaîne;
  nom?: chaîne;
}

Ce service utilise le service HttpClient d'Angular pour effectuer des requêtes HTTP vers notre application serveur (que nous utiliserons dans un instant) et le service Router pour naviguer par programme. Nous devons les injecter dans notre constructeur de services:

 constructeur (http privé: HttpClient, routeur privé: Routeur) {}

Ensuite, nous définissons quatre méthodes qui interagissent avec le jeton JWT. Nous implémentons saveToken pour gérer le stockage du jeton dans localStorage et sur la propriété token une méthode getToken pour récupérer le jeton à partir de ] localStorage ou de la propriété du jeton et une fonction de déconnexion qui supprime le jeton JWT et redirige vers la page d'accueil.

Il est important de noter que ce code ne fonctionne pas ne fonctionne pas si vous utilisez le rendu côté serveur, car les API comme localStorage et window.atob ne sont pas disponibles. Il existe des détails sur les solutions pour traiter le rendu côté serveur dans la documentation Angular .

Jusqu'à présent, cela nous donne:

 import {Injectable} de "@ angular / core";
importer {HttpClient} depuis "@ angular / common / http";
importer {Router} depuis "@ angular / router";
importer {Observable} depuis "rxjs";
importer {map} depuis "rxjs / operators";

interface d'exportation UserDetails {
  _id: chaîne;
  email: chaîne;
  nom: chaîne;
  exp: nombre;
  iat: nombre;
}

interface TokenResponse {
  jeton: chaîne;
}

interface d'exportation TokenPayload {
  email: chaîne;
  mot de passe: chaîne;
  nom?: chaîne;
}

@Injectable ({
  providedIn: "root"
})
classe d'exportation AuthenticationService {
  jeton privé: chaîne;

  constructeur (http privé: HttpClient, routeur privé: routeur) {}

  saveToken privé (jeton: chaîne): void {
    localStorage.setItem ("moyen-jeton", jeton);
    this.token = token;
  }

  getToken () privé: chaîne {
    si (! this.token) {
      this.token = localStorage.getItem ("mean-token");
    }
    renvoyer this.token;
  }

  déconnexion publique (): void {
    this.token = "";
    window.localStorage.removeItem ("moyen-jeton");
    this.router.navigateByUrl ("/");
  }
}

Ajoutons maintenant une méthode pour vérifier ce jeton – et la validité du jeton – pour savoir si le visiteur est connecté.

Obtention de données d'un JWT

Lorsque nous définissons les données pour le JWT (dans la méthode generateJwt Mongoose), nous avons inclus la date d'expiration dans une propriété exp . But if you look at a JWT, it seems to be a random string, like this following example:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NWQ0MjNjMTUxMzcxMmNkMzE3YTRkYTciLCJlbWFpbCI6InNpbW9uQGZ1bGxzdGFja3RyYWluaW5nLmNvbSIsIm5hbWUiOiJTaW1vbiBIb2xtZXMiLCJleHAiOjE0NDA1NzA5NDUsImlhdCI6MTQzOTk2NjE0NX0.jS50GlmolxLoKrA_24LDKaW3vNaY94Y9EqYAFvsTiLg

So how do you read a JWT?

A JWT is actually made up of three separate strings, separated by a dot (.). These three parts are:

  1. Header: an encoded JSON object containing the type and the hashing algorithm used
  2. Payload: an encoded JSON object containing the data, the real body of the token
  3. Signature: an encrypted hash of the header and payload, using the “secret” set on the server.

It’s the second part we’re interested in here — the payload. Note that this is encoded rather than encrypted, meaning that we can decode it.

There’s a function called atob that’s native to modern browsers, and which will decode a Base64 string like this.

So we need to get the second part of the token, decode it and parse it as JSON. Then we can check that the expiry date hasn’t passed.

At the end of it, the getUserDetails function should return an object of the UserDetails type or nulldepending on whether a valid token is found or not. Put together, it looks like this:

public getUserDetails(): UserDetails {
  const token = this.getToken();
  let payload;
  if (token) {
    payload = token.split(".")[1];
    payload = window.atob(payload);
    return JSON.parse(payload);
  } autre {
    return null;
  }
}

The user details that are provided include the information about the user’s name, email, and the expiration of the token, which we’ll use to check if the user session is valid.

Check Whether a User Is Logged In

Add a new method called isLoggedIn to the service. It uses the getUserDetails method to get the token details from the JWT token and checks if the expiration hasn’t passed yet:

public isLoggedIn(): boolean {
  const user = this.getUserDetails();
  if (user) {
    return user.exp > Date.now() / 1000;
  } autre {
    retour faux;
  }
}

If the token exists, the method will return if the user is logged in as a Boolean value. Now we can construct our HTTP requests to load data, using the token for authorization.

Structuring the API Calls

To facilitate making API calls, add the request method to the AuthenticationServicewhich is able to construct and return the proper HTTP request observable depending on the specific type of request. It’s a private method, since it’s only used by this service, and exists just to reduce code duplication. This will use the Angular HttpClient service. Remember to inject this into the AuthenticationService if it’s not already there:

private request(
  method: "post" | "get",
  type: "login" | "register" | "profile",
  user?: TokenPayload
): Observable {
  let base$;

  if (method === "post") {
    base$ = this.http.post(`/api/${type}`, user);
  } autre {
    base$ = this.http.get(`/api/${type}`, {
      headers: { Authorization: `Bearer ${this.getToken()}` }
    });
  }

  const request = base$.pipe(
    map((data: TokenResponse) => {
      if (data.token) {
        this.saveToken(data.token);
      }
      renvoyer des données;
    })
  );

  return request;
}

It does require the map operator from RxJS in order to intercept and store the token in the service if it’s returned by an API login or register call. Now we can implement the public methods to call the API.

Calling the Register and Login API Endpoints

Just three methods to add. We’ll need an interface between the Angular app and the API, to call the login and register endpoints and save the returned token, or the profile endpoint to get the user details:

public register(user: TokenPayload): Observable {
  return this.request("post", "register", user);
}

public login(user: TokenPayload): Observable {
  return this.request("post", "login", user);
}

public profile(): Observable {
  return this.request("get", "profile");
}

Each method returns an observable that will handle the HTTP request for one of the API calls we need to make. That finalizes the service; now it’s time to tie everything together in the Angular app.

Apply Authentication to Angular App

We can use the AuthenticationService inside the Angular app in a number of ways to give the experience we’re after:

  1. wire up the register and sign-in forms
  2. update the navigation to reflect the user’s status
  3. only allow logged-in users to access the /profile route
  4. call the protected /api/profile API route

To get started, we first generate the components we need using Angular CLI:

$ ng generate component register
CREATE src/app/register/register.component.css (0 bytes)
CREATE src/app/register/register.component.html (23 bytes)
CREATE src/app/register/register.component.spec.ts (642 bytes)
CREATE src/app/register/register.component.ts (283 bytes)
UPDATE src/app/app.module.ts (458 bytes)

$ ng generate component profile
CREATE src/app/profile/profile.component.css (0 bytes)
CREATE src/app/profile/profile.component.html (22 bytes)
CREATE src/app/profile/profile.component.spec.ts (635 bytes)
CREATE src/app/profile/profile.component.ts (279 bytes)
UPDATE src/app/app.module.ts (540 bytes)

$ ng generate component login
CREATE src/app/login/login.component.css (0 bytes)
CREATE src/app/login/login.component.html (20 bytes)
CREATE src/app/login/login.component.spec.ts (621 bytes)
CREATE src/app/login/login.component.ts (271 bytes)
UPDATE src/app/app.module.ts (614 bytes)

$ ng generate component home
CREATE src/app/home/home.component.css (0 bytes)
CREATE src/app/home/home.component.html (19 bytes)
CREATE src/app/home/home.component.spec.ts (614 bytes)
CREATE src/app/home/home.component.ts (267 bytes)
UPDATE src/app/app.module.ts (684 bytes)

Connect the Register and Login Controllers

Now that our components have been created, let’s have a look at the register and login forms.

The Register Page

First, let’s create the registration form. It has NgModel directives attached to the fields, all bound to properties set on the credentials controller property. The form also has a (submit) event binding to handle the submission. In the example application, it’s in /client/src/app/register/register.component.html and looks like this:

The first task in the controller is to ensure our AuthenticationService and the Router are injected and available through the constructor. Next, inside the register handler for the form submit, make a call to auth.registerpassing it the credentials from the form.

The register method returns an observable, which we need to subscribe to in order to trigger the request. The observable will emit success or failure, and if someone has successfully registered, we’ll set the application to redirect them to the profile page or log the error in the console.

In the sample application, the controller is in /client/src/app/register/register.component.ts and looks like this:

import { Component } from "@angular/core";
import { AuthenticationService, TokenPayload } from "../authentication.service";
import { Router } from "@angular/router";

@Component({
  templateUrl: "./register.component.html",
  styleUrls: ["./register.component.css"]
})
export class RegisterComponent {
  credentials: TokenPayload = {
    email: "",
    name: "",
    password: ""
  };

  constructor(private auth: AuthenticationService, private router: Router) {}

  register() {
    this.auth.register(this.credentials).subscribe(
      () => {
        this.router.navigateByUrl("/profile");
      },
      err => {
        console.error(err);
      }
    );
  }
}

The Login Page

The login page is very similar in nature to the register page, but in this form we don’t ask for the name, just email and password. In the sample application, it’s in /client/src/app/login/login.component.html and looks like this:

Once again, we have the form submit handler and NgModel attributes for each of the inputs. In the controller, we want the same functionality as the register controller, but this time to call the login method of the AuthenticationService.

In the sample application, the controller is in /client/src/app/login/login.component.ts and look like this:

import { Component } from "@angular/core";
import { AuthenticationService, TokenPayload } from "../authentication.service";
import { Router } from "@angular/router";

@Component({
  templateUrl: "./login.component.html",
  styleUrls: ["./login.component.css"]
})
export class LoginComponent {
  credentials: TokenPayload = {
    email: "",
    password: ""
  };

  constructor(private auth: AuthenticationService, private router: Router) {}

  login() {
    this.auth.login(this.credentials).subscribe(
      () => {
        this.router.navigateByUrl("/profile");
      },
      err => {
        console.error(err);
      }
    );
  }
}

Now users can register and sign in to the application. Note that, again, there should be more validation in the forms to ensure that all required fields are filled before submitting. These examples are kept to the bare minimum to highlight the main functionality.

Change Content Based on User Status

In the navigation, we want to show the Sign in link if a user isn’t logged in, and their username with a link to the profile page if they are logged in. The navbar is found in the App component.

First, we’ll look at the App component controller. We can inject the AuthenticationService into the component and call it directly in our template. In the sample app, the file is in /client/src/app/app.component.ts and looks like this:

import { Component } from "@angular/core";
import { AuthenticationService } from "./authentication.service";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})

export class AppComponent {
  constructor(public auth: AuthenticationService) {}
}

Now, in the associated template we can use auth.isLoggedIn() to determine whether to display the sign-in link or the profile link. To add the user’s name to the profile link, we can access the name property of auth.getUserDetails()?.name. Remember that this is getting the data from the JWT. The ?. operator is a special way to access a property on an object that may be undefined, without throwing an error.

In the sample app, the file is in /client/src/app/app.component.html and the updated part looks like this:




Protect a Route for Logged in Users Only

In this step, we’ll see how to make a route accessible only to logged-in users, by protecting the /profile path.

Angular allows you to define a route guard, which can run a check at several points of the routing life cycle to determine if the route can be loaded. We’ll use the CanActivate hook to tell Angular to load the profile route only if the user is logged in.

To do, this we need to create a route guard:

$ ng generate guard auth
? Which interfaces would you like to implement? CanActivate
CREATE src/app/auth.guard.spec.ts (331 bytes)
CREATE src/app/auth.guard.ts (456 bytes)

It must implement the CanActivate interface, and the associated canActivate method. This method returns a Boolean value from the AuthenticationService.isLoggedIn method (basically checks if the token is found, and still valid), and if the user is not valid also redirects them to the home page.

In auth.guard.ts:

import { Injectable } from "@angular/core";
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
  Router
} from "@angular/router";
import { Observable } from "rxjs";
import { AuthenticationService } from "./authentication.service";

@Injectable({
  providedIn: "root"
})
export class AuthGuard implements CanActivate {
  constructor(private auth: AuthenticationService, private router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable
    | Promise
    | boolean
    | UrlTree {
    if (!this.auth.isLoggedIn()) {
      this.router.navigateByUrl("/");
      retour faux;
    }
    return true;
  }
}

To enable this guard, we have to declare it on the route configuration. There’s a route property called canActivatewhich takes an array of services that should be called before activating the route. The routes are defined in the AppRoutingModulewhich contains the routes like you see here:

const routes: Routes = [
  { path: "", component: HomeComponent },
  { path: "login", component: LoginComponent },
  { path: "register", component: RegisterComponent },
  { path: "profile", component: ProfileComponent, canActivate: [AuthGuard] }
];

The whole file should look like this:

import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { LoginComponent } from "./login/login.component";
import { RegisterComponent } from "./register/register.component";
import { ProfileComponent } from "./profile/profile.component";
import { AuthGuard } from "./auth.guard";

const routes: Routes = [
  { path: "", component: HomeComponent },
  { path: "login", component: LoginComponent },
  { path: "register", component: RegisterComponent },
  { path: "profile", component: ProfileComponent, canActivate: [AuthGuard] }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

With that route guard in place, now if an unauthenticated user tries to visit the profile page, Angular will cancel the route change and redirect to the home page, thus protecting it from unauthenticated users.

Call a Protected API Route

The /api/profile route has been set up to check for a JWT in the request. Otherwise, it will return a 401 unauthorized error.

To pass the token to the API, it needs to be sent through as a header on the request, called Authorization. The following snippet shows the main data service function, and the format required to send the token. The AuthenticationService already handles this, but you can find this in /client/src/app/authentication.service.ts:

base$ = this.http.get(`/api/${type}`, {
  headers: { Authorization: `Bearer ${this.getToken()}` }
});

Remember that the back-end code is validating that the token is genuine when the request is made, by using the secret known only to the issuing server.

To make use of this in the profile page, we just need to update the controller, in /client/src/app/profile/profile.component.ts in the sample app. This will populate a details property when the API returns some data, which should match the UserDetails interface:

import { Component, OnInit } from "@angular/core";
import { AuthenticationService, UserDetails } from "../authentication.service";

@Component({
  templateUrl: "./profile.component.html",
  styleUrls: ["./profile.component.css"]
})
export class ProfileComponent implements OnInit {
  details: UserDetails;

  constructor(private auth: AuthenticationService) {}

  ngOnInit() {
    this.auth.profile().subscribe(
      user => {
        this.details = user;
      },
      err => {
        console.error(err);
      }
    );
  }
}

Then, of course, it’s just a case of updating the bindings in the view (/src/app/profile/profile.component.html). Again, the ?. is a safety operator for binding properties that don’t exist on first render (since data has to load first):

{{ details?.name }}

{{ details?.email }}

Running the Angular App

To run the Angular app, we’re going to need to route any requests to /api to our Express server running on http://localhost:3000/. To do this, create a proxy.conf.json file in the client directory:

touch proxy.conf.json

Also add the following content:

{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false
  }
}

Finally, update the start script in client/package.json:

"start": "ng serve --proxy-config proxy.conf.json",

Now, make sure Mongo is running, start the Express app from within the root of our project using npm start and start the Angular app from within the client directory using the same command.

Then, visit http://localhost:4200to see the (almost) finished product. Attempt to register an account at http://localhost:4200/register and to log in, to assure yourself that everything is working as it should.

Some Final Touches

As you’ll doubtless have noticed, the final app is without any styles. As this is a bit of a lengthy tutorial, I haven’t included them here. But if you take a look at the finished code on GitHub, you can grab everything from there. The files to look at are:

If you copy the extra markup out of these files, you should end up with this:

Screenshot of the profile page

And that’s how to manage authentication in the MEAN stack, from securing API routes and managing user details to working with JWTs and protecting routes.



Source link