Autenticação no Vue.js

Publicados: 2022-03-10
Resumo rápido ↬ Todo aplicativo da Web que lida com dados específicos do usuário precisa implementar autenticação. Saber como fazer isso é importante para os desenvolvedores Vue, e é sobre isso que este artigo pretende destacar. Este tutorial será útil para desenvolvedores iniciantes que desejam aprender sobre autenticação no Vue. Para poder acompanhar, você precisa ter um bom conhecimento de Vue e Vuex.

A autenticação é um recurso muito necessário para aplicativos que armazenam dados do usuário. É um processo de verificação da identidade dos usuários, garantindo que usuários não autorizados não possam acessar dados privados — dados pertencentes a outros usuários. Isso leva a ter rotas restritas que só podem ser acessadas por usuários autenticados. Esses usuários autenticados são verificados usando seus detalhes de login (ou seja, nome de usuário/e-mail e senha) e atribuindo-lhes um token a ser usado para acessar os recursos protegidos de um aplicativo.

Neste artigo, você aprenderá sobre:

  1. Configuração Vuex com Axios
  2. Definindo Rotas
  3. Manipulação de usuários
  4. Manipulando Token Expirado

Dependências

Estaremos trabalhando com as seguintes dependências que auxiliam na autenticação:

  • Axios
    Para enviar e recuperar dados de nossa API
  • Vuex
    Para armazenar dados obtidos de nossa API
  • Vue-Router
    Para navegação e proteção de Rotas

Trabalharemos com essas ferramentas e veremos como elas podem trabalhar juntas para fornecer uma funcionalidade de autenticação robusta para nosso aplicativo.

A API de back-end

Estaremos construindo um site de blog simples, que fará uso desta API. Você pode conferir os documentos para ver os endpoints e como as solicitações devem ser enviadas.

Nos documentos, você notará que poucos endpoints são anexados com um cadeado. Essa é uma maneira de mostrar que apenas usuários autorizados podem enviar solicitações para esses endpoints. Os endpoints irrestritos são os endpoints /register e /login . Um erro com o código de status 401 deve ser retornado quando um usuário não autenticado tenta acessar um endpoint restrito.

Após o login bem-sucedido de um usuário, o token de acesso junto com alguns dados será recebido no aplicativo Vue, que será usado na configuração do cookie e anexado no cabeçalho da solicitação para ser usado em solicitações futuras. O back-end verificará o cabeçalho da solicitação sempre que uma solicitação for feita para um endpoint restrito. Não fique tentado a armazenar o token de acesso no armazenamento local.

(Visualização grande)
Mais depois do salto! Continue lendo abaixo ↓

Projeto de andaime

Usando o Vue CLI, execute o comando abaixo para gerar o aplicativo:

 vue create auth-project

Navegue até sua nova pasta:

 cd auth-project

Adicione o roteador vue e instale mais dependências — vuex e axios:

 vue add router npm install vuex axios

Agora execute seu projeto e você deve ver o que tenho abaixo no seu navegador:

 npm run serve

1. Configuração Vuex com Axios

Axios é uma biblioteca JavaScript que é usada para enviar solicitações do navegador para APIs. De acordo com a documentação do Vuex;

“Vuex é um padrão de gerenciamento de estado + biblioteca para aplicativos Vue.js. Ele serve como um armazenamento centralizado para todos os componentes em um aplicativo, com regras que garantem que o estado só possa ser alterado de maneira previsível.”

O que isso significa? Vuex é uma loja usada em um aplicativo Vue que nos permite salvar dados que estarão disponíveis para cada componente e fornecer maneiras de alterar esses dados. Usaremos o Axios em Vuex para enviar nossas solicitações e fazer alterações em nosso estado (dados). Axios será usado nas actions do Vuex para enviar GET e POST , a resposta obtida será usada no envio de informações para as mutations e que atualiza nossos dados de armazenamento.

Para lidar com a redefinição do Vuex após a atualização, trabalharemos com vuex-persistedstate , uma biblioteca que salva nossos dados Vuex entre recarregamentos de página.

 npm install --save vuex-persistedstate

Agora vamos criar uma nova pasta store em src , para configurar a loja Vuex. Na pasta de store , crie uma nova pasta; modules e um arquivo index.js . É importante observar que você só precisa fazer isso se a pasta não for criada para você automaticamente.

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

Aqui estamos fazendo uso do Vuex e importando um module de autenticação da pasta de modules para nossa loja.

Módulos

Módulos são diferentes segmentos de nossa loja que lidam com tarefas semelhantes em conjunto, incluindo:

  • Estado
  • ações
  • mutações
  • getters

Antes de prosseguirmos, vamos editar nosso arquivo 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 o objeto store da pasta ./store assim como o pacote Axios.

Conforme mencionado anteriormente, o cookie do token de acesso e outros dados necessários obtidos da API precisam ser definidos nos cabeçalhos de solicitação para solicitações futuras. Como usaremos o Axios ao fazer solicitações, precisamos configurar o Axios para fazer uso disso. No trecho acima, fazemos isso usando axios.defaults.withCredentials = true , isso é necessário porque, por padrão, os cookies não são passados ​​pelo Axios.

aaxios.defaults.withCredentials = true é uma instrução para o Axios enviar todas as solicitações com credenciais como; cabeçalhos de autorização, certificados de cliente TLS ou cookies (como no nosso caso).

Definimos nosso axios.defaults.baseURL para nossa solicitação Axios para nossa API Dessa forma, sempre que estamos enviando via Axios, ele faz uso dessa URL base. Com isso, podemos adicionar apenas nossos endpoints como /register e /login às nossas ações sem informar a URL completa a cada vez.

Agora dentro da pasta de modules na store crie um arquivo chamado 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

Em nosso dict de state , vamos definir nossos dados e seus valores padrão:

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

Estamos configurando o valor padrão de state , que é um objeto que contém user e posts com seus valores iniciais como null .

Ações

Ações são funções que são usadas para commit uma mutação para alterar o estado ou podem ser usadas para dispatch , ou seja, chama outra ação. Ele pode ser chamado em diferentes componentes ou visualizações e, em seguida, confirma as mutações do nosso estado;

Registrar ação

Nossa ação Register recebe dados de formulário, envia os dados para nosso ponto de extremidade /register e atribui a resposta a uma variável response . Em seguida, enviaremos nosso nome de username e password do formulário para nossa ação de login . Dessa forma, efetuamos o login do usuário após o cadastro, para que ele seja redirecionado para a 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) },

Ação de login

Aqui é onde acontece a autenticação principal. Quando um usuário preenche seu nome de usuário e senha, ele é passado para um User que é um objeto FormData, a função LogIn pega o objeto User e faz uma solicitação POST ao terminal /login para efetuar login do usuário.

A função Login finalmente confirma o nome de username para a mutação setUser .

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

Criar ação de postagem

Nossa ação CreatePost é uma função que recebe a post e a envia para nosso ponto de extremidade /post e, em seguida, despacha a ação GetPosts . Isso permite que o usuário veja suas postagens após a criação.

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

Obter ação de postagens

Nossa ação GetPosts envia uma solicitação GET para nosso endpoint /posts para buscar as postagens em nossa API e confirma a mutação setPosts .

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

Ação de logout

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

Nossa ação LogOut remove nosso user do cache do navegador. Ele faz isso cometendo um logout :

Mutações

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

Cada mutação recebe o state e um valor da ação que a comete, além de Logout . O valor obtido é usado para alterar certas partes ou todas ou como no LogOut definir todas as variáveis ​​de volta para null.

Getters

Getters são funcionalidades para obter o estado. Ele pode ser usado em vários componentes para obter o estado atual. A função isAuthenticatated verifica se o state.user está definido ou nulo e retorna true ou false respectivamente. StatePosts e StateUser retornam o valor de state.posts e state.user .

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

Agora todo o seu arquivo auth.js deve se parecer com meu código no GitHub.

Configurando Componentes

1. Componentes NavBar.vue e App.vue

Na pasta src/components , exclua o HelloWorld.vue e um novo arquivo chamado NavBar.vue .

Este é o componente para nossa barra de navegação, ele liga para diferentes páginas do nosso componente roteado aqui. Cada link de roteador aponta para uma rota/página em nosso aplicativo.

O v-if="isLoggedIn" é uma condição para exibir o link Logout se um usuário estiver logado e ocultar as rotas Register e Login . Temos um método de logout que só pode ser acessado por usuários conectados, ele será chamado quando o link Logout for clicado. Ele despachará a ação LogOut e, em seguida, direcionará o usuário para a página de login.

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

Agora edite seu componente App.vue para ficar assim:

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

Aqui nós importamos o componente NavBar que criamos acima e colocamos na seção de template antes do <router-view /> .

2. Componentes das Vistas

Os componentes de visualizações são páginas diferentes no aplicativo que serão definidas em uma rota e podem ser acessadas a partir da barra de navegação. Para começar Vá para a pasta views , exclua o componente About.vue e adicione os seguintes componentes:

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

Home.vue

Reescreva o Home.vue para ficar assim:

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

Isso exibirá um texto de boas-vindas aos usuários quando eles visitarem a página inicial.

Register.vue

Esta é a página que queremos que nossos usuários possam se inscrever em nosso aplicativo. Quando os usuários preenchem o formulário, suas informações são enviadas para a API e adicionadas ao banco de dados e, em seguida, logadas.

Olhando para a API, o endpoint /register requer um nome de username , full_name e password de nosso usuário. Agora vamos criar uma página e um formulário para obter essas informações:

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

No componente Register , precisaremos chamar a ação Register que receberá os dados do formulário.

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

Começamos importando mapActions do Vuex, o que isso faz é importar ações da nossa loja para o componente. Isso nos permite chamar a ação do componente.

data() contém o valor do estado local que será usado neste componente, temos um objeto de form que contém username , full_name e password , com seus valores iniciais definidos como uma string vazia. Também temos showError que é um booleano, para ser usado para mostrar um erro ou não.

Nos methods importamos a ação Register usando as Mapactions para o componente, então a ação Register pode ser chamada com this.Register .

Temos um método submit que chama a ação Register à qual temos acesso usando this.Register , enviando this.form . Se nenhum error for encontrado, usamos this.$router para enviar o usuário para a página de login. Caso contrário, configuramos showError como true.

Tendo feito isso, podemos incluir alguns estilos.

 <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

Nossa página de Login é onde os usuários cadastrados, irão digitar seu nome de username e password para serem autenticados pela API e logados em nosso 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>

Agora teremos que passar nossos dados de formulário para a ação que envia a solicitação e depois enviar para a 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 e usamos na importação da ação LogIn para o componente, que será usado em nossa função submit .

Após a ação de Login , o usuário é redirecionado para a página /posts . Em caso de erro, o erro é detectado e ShowError é definido como true.

Agora, alguns estilos:

 <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

Nossa página de Posts é a página segura que está disponível apenas para usuários autenticados. Nesta página, eles têm acesso às postagens no banco de dados da API. Isso permite que os usuários tenham acesso a postagens e também permite que eles criem postagens para a 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>

No código acima, temos um formulário para o usuário poder criar novos posts. O envio do formulário deve fazer com que a postagem seja enviada para a API — adicionaremos o método que faz isso em breve. Também temos uma seção que exibe as postagens obtidas da API (caso o usuário tenha alguma). Se o usuário não tiver nenhuma postagem, simplesmente exibimos uma mensagem informando que não há postagens.

Os getters StateUser e StatePosts são mapeados, ou seja, importados usando mapGetters para Posts.vue e, em seguida, podem ser chamados no modelo.

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

Temos um estado inicial para form , que é um objeto que tem title e write_up como suas chaves e os valores são definidos como uma string vazia. Esses valores serão alterados para o que o usuário inserir no formulário na seção de modelo do nosso componente.

Quando o usuário envia o post, chamamos o this.CreatePost que recebe o objeto do formulário.

Como você pode ver no ciclo de vida created , temos this.GetPosts para buscar as postagens quando o componente é criado.

Alguns estilos,

 <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. Definindo Rotas

Em nosso arquivo router/index.js , importe nossas visualizações e defina rotas para cada uma delas

 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. Lidando com usuários

  • Usuários não autorizados
    Se você notou na definição de nossas rotas de posts adicionamos uma meta chave para indicar que o usuário deve ser autenticado, agora precisamos ter um router.BeforeEach guarda de navegação que verifica se uma rota tem a chave meta: {requiresAuth: true} . Se uma rota tiver a meta chave, ela verificará se há um token no armazenamento; se presente, ele os redireciona para a rota 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
  • Usuários autorizados
    Também temos uma meta nas rotas /register e /login . A meta: {guest: true} impede que os usuários que estão logados acessem as rotas com a 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(); } });

Ao final, seu arquivo deverá ficar assim:

 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. Manipulação de Token Expirado (Solicitações Proibidas)

Nossa API está configurada para expirar tokens após 30 minutos, agora se tentarmos acessar a página de posts após 30 minutos, recebemos um erro 401 , o que significa que temos que fazer login novamente, então definiremos um interceptor que lê se recebermos um 401 , então ele nos redireciona de volta para a página de login .

Adicione o snippet abaixo após a declaração de URL padrão do Axios no arquivo 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') } } })

Isso deve levar seu código para o mesmo estado do exemplo no GitHub.

Conclusão

Se você conseguiu acompanhar até o final, agora deve ser capaz de criar um aplicativo de front-end totalmente funcional e seguro. Agora você aprendeu mais sobre o Vuex e como integrá-lo ao Axios, e também como salvar seus dados após recarregar.

  • O código está disponível no GitHub →

  • Site hospedado: https://nifty-hopper-1e9895.netlify.app/

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

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

Recursos

  • “Manuseando Cookies com Axios”, Aditya Srivastava, Médio
  • “Criando um protetor de navegação de autenticação no Vue”, Laurie Barth, Ten Mile Square Blog
  • “Introdução ao Vuex”, Guia Oficial
  • “Autenticação Vue.js JWT com Vuex e Vue Router,” BezKoder