Authentification dans Vue.js
Publié: 2022-03-10L'authentification est une fonctionnalité très nécessaire pour les applications qui stockent des données utilisateur. Il s'agit d'un processus de vérification de l'identité des utilisateurs, garantissant que les utilisateurs non autorisés ne peuvent pas accéder aux données privées, c'est-à-dire aux données appartenant à d'autres utilisateurs. Cela conduit à avoir des itinéraires restreints qui ne sont accessibles qu'aux utilisateurs authentifiés. Ces utilisateurs authentifiés sont vérifiés en utilisant leurs informations de connexion (c'est-à-dire nom d'utilisateur/e-mail et mot de passe) et en leur attribuant un jeton à utiliser pour accéder aux ressources protégées d'une application.
Dans cet article, vous allez découvrir :
- Configuration de Vuex avec Axios
- Définir des itinéraires
- Gestion des utilisateurs
- Gestion du jeton expiré
Dépendances
Nous allons travailler avec les dépendances suivantes qui aident à l'authentification :
- Axios
Pour envoyer et récupérer des données depuis notre API - Vuex
Pour stocker les données obtenues à partir de notre API - Vue-Routeur
Pour la navigation et la protection des Routes
Nous travaillerons avec ces outils et verrons comment ils peuvent fonctionner ensemble pour fournir une fonctionnalité d'authentification robuste pour notre application.
L'API principale
Nous allons construire un site de blog simple, qui utilisera cette API. Vous pouvez consulter les documents pour voir les points de terminaison et comment les demandes doivent être envoyées.
Dans la documentation, vous remarquerez que peu de points de terminaison sont attachés avec un verrou. C'est un moyen de montrer que seuls les utilisateurs autorisés peuvent envoyer des demandes à ces points de terminaison. Les points de terminaison sans restriction sont les points de terminaison /register
et /login
. Une erreur avec le code d'état 401
doit être renvoyée lorsqu'un utilisateur non authentifié tente d'accéder à un point de terminaison restreint.
Après avoir connecté un utilisateur avec succès, le jeton d'accès ainsi que certaines données seront reçus dans l'application Vue, qui seront utilisées pour définir le cookie et jointes dans l'en-tête de la demande à utiliser pour les demandes futures. Le backend vérifiera l'en-tête de la demande chaque fois qu'une demande est faite à un point de terminaison restreint. Ne soyez pas tenté de stocker le jeton d'accès dans le stockage local.
Projet d'échafaudage
À l'aide de Vue CLI, exécutez la commande ci-dessous pour générer l'application :
vue create auth-project
Accédez à votre nouveau dossier :
cd auth-project
Ajoutez le vue-router et installez plus de dépendances — vuex et axios :
vue add router npm install vuex axios
Exécutez maintenant votre projet et vous devriez voir ce que j'ai ci-dessous sur votre navigateur :
npm run serve
1. Configuration de Vuex avec Axios
Axios est une bibliothèque JavaScript utilisée pour envoyer des requêtes du navigateur aux API. Selon la documentation de Vuex ;
"Vuex est un modèle de gestion d'état + une bibliothèque pour les applications Vue.js. Il sert de magasin centralisé pour tous les composants d'une application, avec des règles garantissant que l'état ne peut être muté que de manière prévisible.
Qu'est-ce que ça veut dire? Vuex est un magasin utilisé dans une application Vue qui nous permet d' enregistrer des données qui seront disponibles pour chaque composant et de fournir des moyens de modifier ces données. Nous utiliserons Axios dans Vuex pour envoyer nos demandes et apporter des modifications à notre état (données). Axios sera utilisé dans les actions
Vuex pour envoyer GET
et POST
, la réponse obtenue sera utilisée pour envoyer des informations aux mutations
et mettre à jour les données de notre magasin.
Pour gérer la réinitialisation de Vuex après l'actualisation, nous allons travailler avec vuex-persistedstate
, une bibliothèque qui enregistre nos données Vuex entre les rechargements de page.
npm install --save vuex-persistedstate
Créons maintenant un nouveau store
de dossiers dans src
, pour configurer le magasin Vuex. Dans le dossier du store
, créez un nouveau dossier ; modules
et un fichier index.js
. Il est important de noter que vous n'avez besoin de le faire que si le dossier n'est pas créé automatiquement pour vous.
import Vuex from 'vuex'; import Vue from 'vue'; import createPersistedState from "vuex-persistedstate"; import auth from './modules/auth'; // Load Vuex Vue.use(Vuex); // Create store export default new Vuex.Store({ modules: { auth }, plugins: [createPersistedState()] });
Ici, nous utilisons Vuex
et importons un module
d'authentification du dossier modules
dans notre magasin.
Modules
Les modules sont différents segments de notre magasin qui gèrent ensemble des tâches similaires, notamment :
- Etat
- Actions
- mutations
- getters
Avant de continuer, éditons notre fichier main.js
import Vue from 'vue' import App from './App.vue' import router from './router'; import store from './store'; import axios from 'axios'; axios.defaults.withCredentials = true axios.defaults.baseURL = 'https://gabbyblog.herokuapp.com/'; Vue.config.productionTip = false new Vue({ store, router, render: h => h(App) }).$mount('#app')
Nous avons importé l'objet store
du dossier ./store
ainsi que le package Axios.
Comme mentionné précédemment, le cookie de jeton d'accès et les autres données nécessaires obtenues de l'API doivent être définis dans les en-têtes de requête pour les requêtes futures. Étant donné que nous utiliserons Axios lors des demandes, nous devons configurer Axios pour l'utiliser. Dans l'extrait ci-dessus, nous le faisons en utilisant axios.defaults.withCredentials = true
, cela est nécessaire car par défaut les cookies ne sont pas transmis par Axios.
aaxios.defaults.withCredentials = true
est une instruction à Axios pour envoyer toutes les demandes avec des informations d'identification telles que ; les en-têtes d'autorisation, les certificats client TLS ou les cookies (comme dans notre cas).
Nous définissons notre axios.defaults.baseURL
pour notre requête Axios à notre API
. Ainsi, chaque fois que nous envoyons via Axios, il utilise cette URL de base. Avec cela, nous pouvons ajouter uniquement nos points de terminaison tels que /register
et /login
à nos actions sans indiquer l'URL complète à chaque fois.
Maintenant, dans le dossier modules
du store
créez un fichier appelé auth.js
//store/modules/auth.js import axios from 'axios'; const state = { }; const getters = { }; const actions = { }; const mutations = { }; export default { state, getters, actions, mutations };
state
Dans notre dict state
, nous allons définir nos données, et leurs valeurs par défaut :
const state = { user: null, posts: null, };
Nous définissons la valeur par défaut de state
, qui est un objet qui contient user
et posts
avec leurs valeurs initiales comme null
.
Actions
Les actions sont des fonctions qui sont utilisées pour commit
une mutation pour changer l'état ou peuvent être utilisées pour dispatch
, c'est-à-dire appeler une autre action. Il peut être appelé dans différents composants ou vues et engage alors des mutations de notre état ;
Enregistrer une action
Notre action Register
prend des données de formulaire, envoie les données à notre point de terminaison /register
et attribue la réponse à une variable response
. Ensuite, nous enverrons notre nom d' username
et password
de passe de formulaire à notre action de login
. De cette façon, nous connectons l'utilisateur après son inscription, il est donc redirigé vers la page /posts
.
async Register({dispatch}, form) { await axios.post('register', form) let UserForm = new FormData() UserForm.append('username', form.username) UserForm.append('password', form.password) await dispatch('LogIn', UserForm) },
Action de connexion
C'est ici que se produit l'authentification principale. Lorsqu'un utilisateur remplit son nom d'utilisateur et son mot de passe, il est transmis à un User
qui est un objet FormData, la fonction LogIn
prend l'objet User
et envoie une requête POST
au point de terminaison /login
pour connecter l'utilisateur.
La fonction Login
valide enfin le nom d' username
dans la mutation setUser
.
async LogIn({commit}, User) { await axios.post('login', User) await commit('setUser', User.get('username')) },
Créer une action de publication
Notre action CreatePost
est une fonction qui prend la post
et l'envoie à notre point de terminaison /post
, puis distribue l'action GetPosts
. Cela permet à l'utilisateur de voir ses publications après leur création.
async CreatePost({dispatch}, post) { await axios.post('post', post) await dispatch('GetPosts') },
Obtenir l'action des publications
Notre action GetPosts
envoie une requête GET
à notre point de terminaison /posts
pour récupérer les publications dans notre API et valide la mutation setPosts
.
async GetPosts({ commit }){ let response = await axios.get('posts') commit('setPosts', response.data) },
Action de déconnexion
async LogOut({commit}){ let user = null commit('logout', user) }
Notre action de LogOut
supprime notre user
du cache du navigateur. Il le fait en commitant une logout
:
mutation
const mutations = { setUser(state, username){ state.user = username }, setPosts(state, posts){ state.posts = posts }, LogOut(state){ state.user = null state.posts = null }, };
Chaque mutation prend l' state
et une valeur de l'action qui la commet, à part Logout
. La valeur obtenue est utilisée pour modifier certaines parties ou toutes ou comme dans LogOut
, remettez toutes les variables à null.
Getters
Les getters sont des fonctionnalités permettant d'obtenir l'état. Il peut être utilisé dans plusieurs composants pour obtenir l'état actuel. La fonction isAuthenticatated
vérifie si state.user
est défini ou null et renvoie respectivement true
ou false
. StatePosts
et StateUser
renvoient respectivement la valeur state.posts
et state.user
.
const getters = { isAuthenticated: state => !!state.user, StatePosts: state => state.posts, StateUser: state => state.user, };
Maintenant, tout votre fichier auth.js
devrait ressembler à mon code sur GitHub.
Configuration des composants
1. Composants NavBar.vue
et App.vue
Dans votre dossier src/components
, supprimez HelloWorld.vue
et un nouveau fichier appelé NavBar.vue
.
Il s'agit du composant de notre barre de navigation, il relie les différentes pages de notre composant qui ont été routées ici. Chaque lien de routeur pointe vers une route/page sur notre application.
Le v-if="isLoggedIn"
est une condition pour afficher le lien de Logout
si un utilisateur est connecté et masquer les routes d' Register
et de Login
. Nous avons une méthode de logout
qui ne peut être accessible qu'aux utilisateurs connectés, elle sera appelée lorsque le lien de Logout
sera cliqué. Il enverra l'action LogOut
puis dirigera l'utilisateur vers la page de connexion.
<template> <div> <router-link to="/">Home</router-link> | <router-link to="/posts">Posts</router-link> | <span v-if="isLoggedIn"> <a @click="logout">Logout</a> </span> <span v-else> <router-link to="/register">Register</router-link> | <router-link to="/login">Login</router-link> </span> </div> </template> <script> export default { name: 'NavBar', computed : { isLoggedIn : function(){ return this.$store.getters.isAuthenticated} }, methods: { async logout (){ await this.$store.dispatch('LogOut') this.$router.push('/login') } }, } </script> <style> #nav { padding: 30px; } #nav a { font-weight: bold; color: #2c3e50; } a:hover { cursor: pointer; } #nav a.router-link-exact-active { color: #42b983; } </style>
Modifiez maintenant votre composant App.vue
pour qu'il ressemble à ceci :
<template> <div> <NavBar /> <router-view/> </div> </template> <script> // @ is an alias to /src import NavBar from '@/components/NavBar.vue' export default { components: { NavBar } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } </style>
Ici, nous avons importé le composant NavBar que nous avons créé ci-dessus et placé dans la section template avant le <router-view />
.
2. Composants des vues
Les composants de vues sont différentes pages de l'application qui seront définies sous un itinéraire et accessibles depuis la barre de navigation. Pour commencer Allez dans le dossier des views
, supprimez le composant About.vue
et ajoutez les composants suivants :
-
Home.vue
-
Register.vue
-
Login.vue
-
Posts.vue
Home.vue
Réécrivez le Home.vue
pour qu'il ressemble à ceci :
<template> <div class="home"> <p>Heyyyyyy welcome to our blog, check out our posts</p> </div> </template> <script> export default { name: 'Home', components: { } } </script>
Cela affichera un texte de bienvenue aux utilisateurs lorsqu'ils visiteront la page d'accueil.
Register.vue
Il s'agit de la page que nous voulons que nos utilisateurs puissent s'inscrire sur notre application. Lorsque les utilisateurs remplissent le formulaire, leurs informations sont envoyées à l'API et ajoutées à la base de données, puis connectées.
En regardant l'API, le point de terminaison /register
nécessite un nom d' username
, un nom full_name
et un mot de password
de notre utilisateur. Créons maintenant une page et un formulaire pour obtenir ces informations :
<template> <div class="register"> <div> <form @submit.prevent="submit"> <div> <label for="username">Username:</label> <input type="text" name="username" v-model="form.username"> </div> <div> <label for="full_name">Full Name:</label> <input type="text" name="full_name" v-model="form.full_name"> </div> <div> <label for="password">Password:</label> <input type="password" name="password" v-model="form.password"> </div> <button type="submit"> Submit</button> </form> </div> <p v-if="showError">Username already exists</p> </div> </template>
Dans le composant Register
, nous devrons appeler l'action Register
qui recevra les données du formulaire.
<script> import { mapActions } from "vuex"; export default { name: "Register", components: {}, data() { return { form: { username: "", full_name: "", password: "", }, showError: false }; }, methods: { ...mapActions(["Register"]), async submit() { try { await this.Register(this.form); this.$router.push("/posts"); this.showError = false } catch (error) { this.showError = true } }, }, }; </script>
Nous commençons par importer mapActions
depuis Vuex, ce que cela fait est d'importer des actions de notre magasin vers le composant. Cela nous permet d'appeler l'action depuis le composant.
data()
contient la valeur d'état local qui sera utilisée dans ce composant, nous avons un objet de form
qui contient username
, full_name
et password
, avec leurs valeurs initiales définies sur une chaîne vide. Nous avons également showError
qui est un booléen, à utiliser pour afficher ou non une erreur.
Dans les methods
nous importons l'action Register
à l'aide des Mapactions
dans le composant, de sorte que l'action Register
puisse être appelée avec this.Register
.
Nous avons une méthode submit qui appelle l'action Register
à laquelle nous avons accès en utilisant this.Register
, en lui envoyant this.form
. Si aucune error
n'est rencontrée, nous utilisons this.$router
pour envoyer l'utilisateur à la page de connexion. Sinon, nous définissons showError
sur true.
Cela fait, nous pouvons inclure un peu de style.
<style scoped> * { box-sizing: border-box; } label { padding: 12px 12px 12px 0; display: inline-block; } button[type=submit] { background-color: #4CAF50; color: white; padding: 12px 20px; cursor: pointer; border-radius:30px; } button[type=submit]:hover { background-color: #45a049; } input { margin: 5px; box-shadow:0 0 15px 4px rgba(0,0,0,0.06); padding:10px; border-radius:30px; } #error { color: red; } </style>
Login.vue
Notre page de connexion est l'endroit où les utilisateurs enregistrés entreront leur nom d' username
et leur mot de password
pour s'authentifier par l'API et se connecter à notre site.
<template> <div class="login"> <div> <form @submit.prevent="submit"> <div> <label for="username">Username:</label> <input type="text" name="username" v-model="form.username" /> </div> <div> <label for="password">Password:</label> <input type="password" name="password" v-model="form.password" /> </div> <button type="submit">Submit</button> </form> <p v-if="showError">Username or Password is incorrect</p> </div> </div> </template>
Maintenant, nous devrons transmettre nos données de formulaire à l'action qui envoie la demande, puis les pousser vers la page sécurisée Posts
<script> import { mapActions } from "vuex"; export default { name: "Login", components: {}, data() { return { form: { username: "", password: "", }, showError: false }; }, methods: { ...mapActions(["LogIn"]), async submit() { const User = new FormData(); User.append("username", this.form.username); User.append("password", this.form.password); try { await this.LogIn(User); this.$router.push("/posts"); this.showError = false } catch (error) { this.showError = true } }, }, }; </script>
Nous importons Mapactions
et l'utilisons pour importer l'action LogIn
dans le composant, qui sera utilisé dans notre fonction submit
.
Après l'action de Login
, l'utilisateur est redirigé vers la page /posts
. En cas d'erreur, l'erreur est détectée et ShowError
est défini sur true.
Maintenant, un peu de style :
<style scoped> * { box-sizing: border-box; } label { padding: 12px 12px 12px 0; display: inline-block; } button[type=submit] { background-color: #4CAF50; color: white; padding: 12px 20px; cursor: pointer; border-radius:30px; } button[type=submit]:hover { background-color: #45a049; } input { margin: 5px; box-shadow:0 0 15px 4px rgba(0,0,0,0.06); padding:10px; border-radius:30px; } #error { color: red; } </style>
Posts.vue
Notre page Messages est la page sécurisée qui n'est disponible que pour les utilisateurs authentifiés. Sur cette page, ils ont accès aux publications de la base de données de l'API. Cela permet aux utilisateurs d'avoir accès aux publications et leur permet également de créer des publications sur l'API.
<template> <div class="posts"> <div v-if="User"> <p>Hi {{User}}</p> </div> <div> <form @submit.prevent="submit"> <div> <label for="title">Title:</label> <input type="text" name="title" v-model="form.title"> </div> <div> <textarea name="write_up" v-model="form.write_up" placeholder="Write up..."></textarea> </div> <button type="submit"> Submit</button> </form> </div> <div class="posts" v-if="Posts"> <ul> <li v-for="post in Posts" :key="post.id"> <div> <p>{{post.title}}</p> <p>{{post.write_up}}</p> <p>Written By: {{post.author.username}}</p> </div> </li> </ul> </div> <div v-else> Oh no!!! We have no posts </div> </div> </template>
Dans le code ci-dessus, nous avons un formulaire permettant à l'utilisateur de créer de nouveaux messages. La soumission du formulaire devrait entraîner l'envoi du message à l'API - nous ajouterons la méthode qui le fait sous peu. Nous avons également une section qui affiche les publications obtenues à partir de l'API (au cas où l'utilisateur en aurait). Si l'utilisateur n'a pas de messages, nous affichons simplement un message indiquant qu'il n'y a pas de messages.
Les getters StateUser
et StatePosts
sont mappés, c'est-à-dire importés à l'aide mapGetters
dans Posts.vue
, puis ils peuvent être appelés dans le modèle.
<script> import { mapGetters, mapActions } from "vuex"; export default { name: 'Posts', components: { }, data() { return { form: { title: '', write_up: '', } }; }, created: function () { // a function to call getposts action this.GetPosts() }, computed: { ...mapGetters({Posts: "StatePosts", User: "StateUser"}), }, methods: { ...mapActions(["CreatePost", "GetPosts"]), async submit() { try { await this.CreatePost(this.form); } catch (error) { throw "Sorry you can't make a post now!" } }, } }; </script>
Nous avons un état initial pour form
, qui est un objet qui a title
et write_up
comme clés et les valeurs sont définies sur une chaîne vide. Ces valeurs seront remplacées par tout ce que l'utilisateur entrera dans le formulaire dans la section modèle de notre composant.
Lorsque l'utilisateur soumet le message, nous appelons le this.CreatePost
qui reçoit l'objet de formulaire.
Comme vous pouvez le voir dans le cycle de vie created
, nous avons this.GetPosts
pour récupérer les publications lorsque le composant est créé.
Un peu de style,
<style scoped> * { box-sizing: border-box; } label { padding: 12px 12px 12px 0; display: inline-block; } button[type=submit] { background-color: #4CAF50; color: white; padding: 12px 20px; cursor: pointer; border-radius:30px; margin: 10px; } button[type=submit]:hover { background-color: #45a049; } input { width:60%; margin: 15px; border: 0; box-shadow:0 0 15px 4px rgba(0,0,0,0.06); padding:10px; border-radius:30px; } textarea { width:75%; resize: vertical; padding:15px; border-radius:15px; border:0; box-shadow:0 0 15px 4px rgba(0,0,0,0.06); height:150px; margin: 15px; } ul { list-style: none; } #post-div { border: 3px solid #000; width: 500px; margin: auto; margin-bottom: 5px;; } </style>
2. Définir les itinéraires
Dans notre fichier router/index.js
, importez nos vues et définissez des routes pour chacune d'elles
import Vue from 'vue' import VueRouter from 'vue-router' import store from '../store'; import Home from '../views/Home.vue' import Register from '../views/Register' import Login from '../views/Login' import Posts from '../views/Posts' Vue.use(VueRouter) const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/register', name: "Register", component: Register, meta: { guest: true }, }, { path: '/login', name: "Login", component: Login, meta: { guest: true }, }, { path: '/posts', name: Posts, component: Posts, meta: {requiresAuth: true}, } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router
3. Gestion des utilisateurs
- Utilisateurs non autorisés
Si vous avez remarqué dans la définition des routes de nos publications nous avons ajouté une clémeta
pour indiquer que l'utilisateur doit être authentifié, maintenant nous avons besoin d'unrouter.BeforeEach
garde de navigation qui vérifie si une route a la clémeta: {requiresAuth: true}
. Si une route a lameta
-clé, elle recherche un jeton dans le magasin ; s'il est présent, il les redirige vers la route delogin
.
const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) router.beforeEach((to, from, next) => { if(to.matched.some(record => record.meta.requiresAuth)) { if (store.getters.isAuthenticated) { next() return } next('/login') } else { next() } }) export default router
- Utilisateurs autorisés
Nous avons également unemeta
sur les routes/register
et/login
. Lameta: {guest: true}
empêche les utilisateurs connectés d'accéder aux routes avec la métaguest
.
router.beforeEach((to, from, next) => { if (to.matched.some((record) => record.meta.guest)) { if (store.getters.isAuthenticated) { next("/posts"); return; } next(); } else { next(); } });
Au final, votre fichier devrait ressembler à ceci :
import Vue from "vue"; import VueRouter from "vue-router"; import store from "../store"; import Home from "../views/Home.vue"; import Register from "../views/Register"; import Login from "../views/Login"; import Posts from "../views/Posts"; Vue.use(VueRouter); const routes = [ { path: "/", name: "Home", component: Home, }, { path: "/register", name: "Register", component: Register, meta: { guest: true }, }, { path: "/login", name: "Login", component: Login, meta: { guest: true }, }, { path: "/posts", name: "Posts", component: Posts, meta: { requiresAuth: true }, }, ]; const router = new VueRouter({ mode: "history", base: process.env.BASE_URL, routes, }); router.beforeEach((to, from, next) => { if (to.matched.some((record) => record.meta.requiresAuth)) { if (store.getters.isAuthenticated) { next(); return; } next("/login"); } else { next(); } }); router.beforeEach((to, from, next) => { if (to.matched.some((record) => record.meta.guest)) { if (store.getters.isAuthenticated) { next("/posts"); return; } next(); } else { next(); } }); export default router;
4.Traitement du jeton expiré (demandes interdites)
Notre API est configurée pour expirer les jetons après 30 minutes, maintenant si nous essayons d'accéder à la page des posts
après 30 minutes, nous obtenons une erreur 401
, ce qui signifie que nous devons nous reconnecter, nous allons donc définir un intercepteur qui lit si nous obtenons un Erreur 401
, puis il nous redirige vers la page de login
.
Ajoutez l'extrait ci-dessous après la déclaration d'URL par défaut d'Axios dans le fichier main.js
axios.interceptors.response.use(undefined, function (error) { if (error) { const originalRequest = error.config; if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; store.dispatch('LogOut') return router.push('/login') } } })
Cela devrait amener votre code au même état que l'exemple sur GitHub.
Conclusion
Si vous avez été en mesure de suivre jusqu'à la fin, vous devriez maintenant être en mesure de créer une application frontale entièrement fonctionnelle et sécurisée. Vous en savez maintenant plus sur Vuex et sur la façon de l'intégrer à Axios, ainsi que sur la sauvegarde de ses données après le rechargement.
Le code est disponible sur GitHub →
Site hébergé :
https://nifty-hopper-1e9895.netlify.app/
API :
https://gabbyblog.herokuapp.com
Documentation API :
https://gabbyblog.herokuapp.com/docs
Ressources
- "Gérer les cookies avec Axios", Aditya Srivastava, Medium
- "Création d'une protection de navigation d'authentification dans Vue", Laurie Barth, blog Ten Mile Square
- « Premiers pas avec Vuex », Guide officiel
- "Authentification Vue.js JWT avec Vuex et Vue Router", BezKoder