Autenticación en Vue.js

Publicado: 2022-03-10
Resumen rápido ↬ Todas las aplicaciones web que manejan datos específicos del usuario deben implementar la autenticación. Saber cómo hacer esto es importante para los desarrolladores de Vue, y eso es lo que este artículo pretende resaltar. Este tutorial resultará útil para los desarrolladores principiantes que quieran aprender sobre la autenticación en Vue. Para poder seguir, deberá tener un buen conocimiento de Vue y Vuex.

La autenticación es una función muy necesaria para las aplicaciones que almacenan datos de usuario. Es un proceso de verificación de la identidad de los usuarios, lo que garantiza que los usuarios no autorizados no puedan acceder a datos privados, datos que pertenecen a otros usuarios. Esto lleva a tener rutas restringidas a las que solo pueden acceder los usuarios autenticados. Estos usuarios autenticados se verifican utilizando sus datos de inicio de sesión (es decir, nombre de usuario/correo electrónico y contraseña) y asignándoles un token que se usará para acceder a los recursos protegidos de una aplicación.

En este artículo, aprenderá sobre:

  1. Configuración de Vuex con Axios
  2. Definición de rutas
  3. Manejo de usuarios
  4. Manejo de token caducado

dependencias

Estaremos trabajando con las siguientes dependencias que ayudan en la autenticación:

  • Axios
    Para enviar y recuperar datos de nuestra API
  • Vuex
    Para almacenar datos obtenidos de nuestra API
  • Vue-Router
    Para navegación y protección de Rutas

Trabajaremos con estas herramientas y veremos cómo pueden trabajar juntas para proporcionar una funcionalidad de autenticación sólida para nuestra aplicación.

La API de back-end

Construiremos un sitio de blog simple, que hará uso de esta API. Puede consultar los documentos para ver los puntos finales y cómo se deben enviar las solicitudes.

En los documentos, notará que algunos puntos finales están conectados con un candado. Esta es una forma de mostrar que solo los usuarios autorizados pueden enviar solicitudes a esos puntos finales. Los puntos finales sin restricciones son los puntos finales /register y /login . Se debe devolver un error con el código de estado 401 cuando un usuario no autenticado intenta acceder a un punto final restringido.

Después de iniciar sesión con éxito en un usuario, el token de acceso junto con algunos datos se recibirán en la aplicación Vue, que se usarán para configurar la cookie y se adjuntarán en el encabezado de la solicitud para usar en futuras solicitudes. El backend verificará el encabezado de la solicitud cada vez que se realice una solicitud a un punto final restringido. No caiga en la tentación de almacenar el token de acceso en el almacenamiento local.

(Vista previa grande)
¡Más después del salto! Continúe leyendo a continuación ↓

Proyecto de andamio

Con Vue CLI, ejecute el siguiente comando para generar la aplicación:

 vue create auth-project

Navega a tu nueva carpeta:

 cd auth-project

Agregue el vue-router e instale más dependencias: vuex y axios:

 vue add router npm install vuex axios

Ahora ejecute su proyecto y debería ver lo que tengo a continuación en su navegador:

 npm run serve

1. Configuración de Vuex con Axios

Axios es una biblioteca de JavaScript que se utiliza para enviar solicitudes desde el navegador a las API. Según la documentación de Vuex;

“Vuex es un patrón de gestión de estado + biblioteca para aplicaciones Vue.js. Sirve como un almacén centralizado para todos los componentes de una aplicación, con reglas que garantizan que el estado solo se puede mutar de manera predecible”.

¿Que significa eso? Vuex es una tienda utilizada en una aplicación Vue que nos permite guardar datos que estarán disponibles para cada componente y proporcionar formas de cambiar dichos datos. Usaremos Axios en Vuex para enviar nuestras solicitudes y realizar cambios en nuestro estado (datos). Axios se usará en las actions de Vuex para enviar GET y POST , la respuesta obtenida se usará para enviar información a las mutations y actualizar los datos de nuestra tienda.

Para lidiar con el reinicio de Vuex después de actualizar, trabajaremos con vuex-persistedstate , una biblioteca que guarda nuestros datos de Vuex entre recargas de página.

 npm install --save vuex-persistedstate

Ahora vamos a crear una nueva store de carpetas en src , para configurar la tienda Vuex. En la carpeta de la store , cree una nueva carpeta; modules y un archivo index.js . Es importante tener en cuenta que solo necesita hacer esto si la carpeta no se crea automáticamente.

 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()] });

Aquí estamos haciendo uso de Vuex e importando un module de autenticación de la carpeta de modules a nuestra tienda.

Módulos

Los módulos son diferentes segmentos de nuestra tienda que manejan tareas similares juntas, que incluyen:

  • estado
  • comportamiento
  • mutaciones
  • captadores

Antes de continuar, editemos nuestro archivo 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')

Importamos el objeto de la store desde la carpeta ./store , así como el paquete Axios.

Como se mencionó anteriormente, la cookie del token de acceso y otros datos necesarios obtenidos de la API deben configurarse en los encabezados de solicitud para futuras solicitudes. Dado que utilizaremos Axios al realizar solicitudes, debemos configurar Axios para hacer uso de esto. En el fragmento anterior, lo hacemos usando axios.defaults.withCredentials = true , esto es necesario porque, de forma predeterminada, Axios no pasa las cookies.

aaxios.defaults.withCredentials = true es una instrucción a Axios para enviar todas las solicitudes con credenciales como; encabezados de autorización, certificados de cliente TLS o cookies (como en nuestro caso).

Establecemos nuestro axios.defaults.baseURL para nuestra solicitud de Axios a nuestra API De esta manera, cada vez que enviamos a través de Axios, hace uso de esta URL base. Con eso, podemos agregar solo nuestros puntos finales como /register e /login a nuestras acciones sin indicar la URL completa cada vez.

Ahora, dentro de la carpeta de modules en la store cree un archivo llamado 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

En nuestro dictado de state , vamos a definir nuestros datos y sus valores predeterminados:

 const state = { user: null, posts: null, };

Estamos configurando el valor predeterminado de state , que es un objeto que contiene user y posts con sus valores iniciales como null .

Comportamiento

Las acciones son funciones que se utilizan para commit una mutación para cambiar el estado o se pueden utilizar para dispatch , es decir, llama a otra acción. Puede ser llamado en diferentes componentes o vistas y luego comete mutaciones de nuestro estado;

Registrar Acción

Nuestra acción de Register toma datos de formulario, envía los datos a nuestro punto final /register y asigna la respuesta a una response variable. A continuación, enviaremos el nombre de username y password de nuestro formulario a nuestra acción de inicio de login . De esta manera, iniciamos sesión en el usuario después de que se registre, por lo que se le redirige a la página /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) },

Acción de inicio de sesión

Aquí es donde ocurre la autenticación principal. Cuando un usuario completa su nombre de usuario y contraseña, se pasa a un User que es un objeto LogIn , la función de inicio de sesión toma el objeto User y realiza una solicitud POST al punto final /login para iniciar la sesión del usuario.

La función de inicio de Login finalmente asigna el nombre de username a la mutación setUser .

 async LogIn({commit}, User) { await axios.post('login', User) await commit('setUser', User.get('username')) },

Crear acción posterior

Nuestra acción CreatePost es una función que toma la post y la envía a nuestro punto final /post , y luego envía la acción GetPosts . Esto permite al usuario ver sus publicaciones después de la creación.

 async CreatePost({dispatch}, post) { await axios.post('post', post) await dispatch('GetPosts') },

Obtener acción de publicaciones

Nuestra acción GetPosts envía una solicitud GET a nuestro punto final /posts para obtener las publicaciones en nuestra API y comete la mutación setPosts .

 async GetPosts({ commit }){ let response = await axios.get('posts') commit('setPosts', response.data) },

Acción de cerrar sesión

 async LogOut({commit}){ let user = null commit('logout', user) }

Nuestra acción LogOut elimina a nuestro user de la memoria caché del navegador. Lo hace al cometer un cierre de logout :

Mutaciones

 const mutations = { setUser(state, username){ state.user = username }, setPosts(state, posts){ state.posts = posts }, LogOut(state){ state.user = null state.posts = null }, };

Cada mutación toma el state y un valor de la acción que la comete, además de Logout . El valor obtenido se usa para cambiar ciertas partes o todas o como en LogOut establecer todas las variables de nuevo en nulo.

captadores

Los captadores son funcionalidades para obtener el estado. Se puede utilizar en varios componentes para obtener el estado actual. La función isAuthenticatated comprueba si state.user está definido o es nulo y devuelve true o false , respectivamente. StatePosts y StateUser devuelven state.posts y state.user valor respectivamente.

 const getters = { isAuthenticated: state => !!state.user, StatePosts: state => state.posts, StateUser: state => state.user, };

Ahora todo su archivo auth.js debería parecerse a mi código en GitHub.

Configuración de componentes

1. Componentes NavBar.vue y App.vue

En su carpeta src/components , elimine HelloWorld.vue y un nuevo archivo llamado NavBar.vue .

Este es el componente de nuestra barra de navegación, se vincula a diferentes páginas de nuestro componente que se ha enrutado aquí. Cada enlace del enrutador apunta a una ruta/página en nuestra aplicación.

El v-if="isLoggedIn" es una condición para mostrar el enlace Logout si un usuario ha iniciado sesión y ocultar las rutas Register e Login . Tenemos un método de logout de sesión al que solo pueden acceder los usuarios registrados, se llamará cuando se haga clic en el enlace Logout . LogOut la acción Cerrar sesión y luego dirigirá al usuario a la página de inicio de sesión.

 <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>

Ahora edite su componente App.vue para que se vea así:

 <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>

Aquí importamos el componente NavBar que creamos arriba y colocamos en la sección de plantilla antes de <router-view /> .

2. Componentes de vistas

Los componentes de vistas son diferentes páginas de la aplicación que se definirán en una ruta y se puede acceder a ellas desde la barra de navegación. Para comenzar, vaya a la carpeta de views , elimine el componente About.vue y agregue los siguientes componentes:

  • Home.vue
  • Register.vue
  • Login.vue
  • Posts.vue

Home.vue

Vuelva a Home.vue para que se vea así:

 <template> <div class="home"> <p>Heyyyyyy welcome to our blog, check out our posts</p> </div> </template> <script> export default { name: 'Home', components: { } } </script>

Esto mostrará un texto de bienvenida a los usuarios cuando visiten la página de inicio.

Register.vue

Esta es la página que queremos que nuestros usuarios puedan registrarse en nuestra aplicación. Cuando los usuarios completan el formulario, su información se envía a la API y se agrega a la base de datos y luego inicia sesión.

Mirando la API, el punto final /register requiere un nombre de username , nombre full_name y password de nuestro usuario. Ahora vamos a crear una página y un formulario para obtener esa información:

 <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>

En el componente Register , necesitaremos llamar a la acción Register que recibirá los datos del formulario.

 <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>

Empezamos importando mapActions de Vuex, lo que hace es importar acciones de nuestra tienda al componente. Esto nos permite llamar a la acción desde el componente.

data() contiene el valor del estado local que se utilizará en este componente, tenemos un objeto de form que contiene nombre de username , full_name y password , con sus valores iniciales establecidos en una cadena vacía. También tenemos showError , que es un valor booleano, que se utilizará para mostrar un error o no.

En los methods , importamos la acción Register utilizando Mapactions en el componente, por lo que la acción Register se puede llamar con this.Register .

Tenemos un método de envío que llama a la acción Register a la que tenemos acceso usando this.Register , enviándolo a this.form . Si no se encuentra ningún error , usamos this.$router para enviar al usuario a la página de inicio de sesión. De lo contrario, establecemos showError en verdadero.

Habiendo hecho eso, podemos incluir algo de estilo.

 <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

Nuestra página de inicio de sesión es donde los usuarios registrados ingresarán su nombre de username y password para ser autenticados por la API e iniciar sesión en nuestro sitio.

 <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>

Ahora tendremos que pasar los datos de nuestro formulario a la acción que envía la solicitud y luego enviarlos a la página segura 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>

Importamos Mapactions y lo usamos para importar la acción LogIn sesión en el componente, que se usará en nuestra función de submit .

Después de la acción Login , se redirige al usuario a la página /posts . En caso de error, el error se detecta y ShowError se establece en verdadero.

Ahora, un poco de estilo:

 <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

Nuestra página de Publicaciones es la página segura que solo está disponible para usuarios autenticados. En esta página, obtienen acceso a las publicaciones en la base de datos de la API. Esto permite a los usuarios tener acceso a las publicaciones y también les permite crear publicaciones en la 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>

En el código anterior, tenemos un formulario para que el usuario pueda crear nuevas publicaciones. Enviar el formulario debería hacer que la publicación se envíe a la API; agregaremos el método que lo hace en breve. También tenemos una sección que muestra publicaciones obtenidas de la API (en caso de que el usuario tenga alguna). Si el usuario no tiene publicaciones, simplemente mostramos un mensaje de que no hay publicaciones.

Los StateUser y StatePosts se asignan, es decir, se importan utilizando mapGetters en Posts.vue y luego se pueden llamar en la plantilla.

 <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>

Tenemos un estado inicial para form , que es un objeto que tiene title y write_up como claves y los valores se establecen en una cadena vacía. Estos valores cambiarán a lo que el usuario ingrese en el formulario en la sección de plantilla de nuestro componente.

Cuando el usuario envía la publicación, llamamos a this.CreatePost que recibe el objeto de formulario.

Como puede ver en el ciclo de vida created , tenemos this.GetPosts para obtener publicaciones cuando se crea el componente.

Algo de estilismo,

 <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. Definición de rutas

En nuestro archivo router/index.js , importe nuestras vistas y defina rutas para cada una de ellas

 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. Manejo de usuarios

  • Usuarios no autorizados
    Si notó que al definir nuestras rutas de publicaciones, agregamos una clave meta para indicar que el usuario debe estar autenticado, ahora necesitamos tener un router.BeforeEach de cada guardia de navegación que verifica si una ruta tiene la clave meta: {requiresAuth: true} . Si una ruta tiene la clave meta , verifica la tienda en busca de un token; si está presente, los redirige a la ruta de inicio de login .
 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
  • Usuarios autorizados
    También tenemos una meta sobre las rutas /register y /login . El meta: {guest: true} evita que los usuarios que han iniciado sesión accedan a las rutas con el meta guest .
 router.beforeEach((to, from, next) => { if (to.matched.some((record) => record.meta.guest)) { if (store.getters.isAuthenticated) { next("/posts"); return; } next(); } else { next(); } });

Al final, su archivo debería ser así:

 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. Manejo de token caducado (solicitudes prohibidas)

Nuestra API está configurada para caducar tokens después de 30 minutos, ahora si intentamos acceder a la página de posts después de 30 minutos, obtenemos un error 401 , lo que significa que tenemos que iniciar sesión nuevamente, por lo que configuraremos un interceptor que lea si obtenemos un 401 , luego nos redirige a la página de inicio de login .

Agregue el fragmento a continuación después de la declaración de URL predeterminada de Axios en el archivo 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') } } })

Esto debería llevar su código al mismo estado que el ejemplo en GitHub.

Conclusión

Si ha podido seguir hasta el final, ahora debería poder crear una aplicación de front-end completamente funcional y segura. Ahora ha aprendido más sobre Vuex y cómo integrarlo con Axios, y también cómo guardar sus datos después de recargar.

  • El código está disponible en GitHub →

  • Sitio alojado: https://nifty-hopper-1e9895.netlify.app/

  • API: https://gabbyblog.herokuapp.com

  • Documentos API: https://gabbyblog.herokuapp.com/docs

Recursos

  • “Manejo de cookies con Axios”, Aditya Srivastava, Medium
  • "Creación de un protector de navegación de autenticación en Vue", Laurie Barth, blog de Ten Mile Square
  • “Primeros pasos con Vuex”, Guía oficial
  • "Autenticación Vue.js JWT con Vuex y Vue Router", BezKoder