Autenticação no Vue.js
Publicados: 2022-03-10A 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:
- Configuração Vuex com Axios
- Definindo Rotas
- Manipulação de usuários
- 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.
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 umameta
chave para indicar que o usuário deve ser autenticado, agora precisamos ter umrouter.BeforeEach
guarda de navegação que verifica se uma rota tem a chavemeta: {requiresAuth: true}
. Se uma rota tiver ameta
chave, ela verificará se há um token no armazenamento; se presente, ele os redireciona para a rota 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
- Usuários autorizados
Também temos umameta
nas rotas/register
e/login
. Ameta: {guest: true}
impede que os usuários que estão logados acessem as rotas com a metaguest
.
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