Аутентификация в Vue.js
Опубликовано: 2022-03-10Аутентификация — очень необходимая функция для приложений, хранящих пользовательские данные. Это процесс проверки личности пользователей, гарантирующий, что неавторизованные пользователи не смогут получить доступ к личным данным — данным, принадлежащим другим пользователям. Это приводит к ограничению маршрутов, к которым могут получить доступ только аутентифицированные пользователи. Эти аутентифицированные пользователи проверяются с использованием их регистрационных данных (т. е. имени пользователя/электронной почты и пароля) и присвоения им токена, который будет использоваться для доступа к защищенным ресурсам приложения.
В этой статье вы узнаете о:
- Конфигурация Vuex с Axios
- Определение маршрутов
- Работа с пользователями
- Обработка токена с истекшим сроком действия
Зависимости
Мы будем работать со следующими зависимостями, которые помогают в аутентификации:
- Аксиос
Для отправки и получения данных из нашего API - Вьюекс
Для хранения данных, полученных от нашего API - Vue-маршрутизатор
Для навигации и защиты маршрутов
Мы будем работать с этими инструментами и посмотрим, как они могут работать вместе, чтобы обеспечить надежные функции аутентификации для нашего приложения.
Серверный API
Мы будем создавать простой блог-сайт, который будет использовать этот API. Вы можете проверить документы, чтобы увидеть конечные точки и способы отправки запросов.
Из документов вы заметите, что несколько конечных точек прикреплены к замку. Это способ показать, что только авторизованные пользователи могут отправлять запросы на эти конечные точки. Неограниченными конечными точками являются конечные точки /register
и /login
. Ошибка с кодом состояния 401
должна возвращаться, когда пользователь, не прошедший проверку подлинности, пытается получить доступ к конечной точке с ограниченным доступом.
После успешного входа пользователя в приложение Vue будет получен токен доступа вместе с некоторыми данными, которые будут использоваться для настройки файла cookie и прикреплены к заголовку запроса для использования в будущих запросах. Серверная часть будет проверять заголовок запроса каждый раз, когда запрос отправляется к ограниченной конечной точке. Не поддавайтесь искушению хранить токен доступа в локальном хранилище.

Проект строительных лесов
Используя Vue CLI, выполните приведенную ниже команду, чтобы сгенерировать приложение:
vue create auth-project
Перейдите в новую папку:
cd auth-project
Добавьте vue-router и установите дополнительные зависимости — vuex и axios:
vue add router npm install vuex axios
Теперь запустите свой проект, и вы должны увидеть в своем браузере то, что показано ниже:
npm run serve
1. Конфигурация Vuex с Axios
Axios — это библиотека JavaScript, которая используется для отправки запросов из браузера в API. Согласно документации Vuex;
«Vuex — это шаблон управления состоянием + библиотека для приложений Vue.js. Он служит централизованным хранилищем для всех компонентов приложения с правилами, гарантирующими, что состояние может изменяться только предсказуемым образом».
Что это значит? Vuex — это хранилище, используемое в приложении Vue, которое позволяет нам сохранять данные, которые будут доступны каждому компоненту, и предоставляет способы изменения таких данных. Мы будем использовать Axios в Vuex для отправки наших запросов и внесения изменений в наше состояние (данные). Axios будет использоваться в actions
Vuex для отправки GET
и POST
, полученный ответ будет использоваться для отправки информации в mutations
и для обновления данных нашего хранилища.
Чтобы справиться со сбросом Vuex после обновления, мы будем работать с vuex-persistedstate
— библиотекой, которая сохраняет данные Vuex между перезагрузками страницы.
npm install --save vuex-persistedstate
Теперь давайте создадим новое store
папок в src
для настройки хранилища Vuex. В папке store
создайте новую папку; modules
и файл index.js
. Важно отметить, что вам нужно сделать это только в том случае, если папка не создается для вас автоматически.
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()] });
Здесь мы используем Vuex
и импортируем module
аутентификации из папки modules
в наш магазин.
Модули
Модули — это разные сегменты нашего магазина, которые совместно решают схожие задачи, в том числе:
- состояние
- действия
- мутации
- добытчики
Прежде чем мы продолжим, давайте отредактируем наш файл 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')
Мы импортировали объект store
из папки ./store
, а также из пакета Axios.
Как упоминалось ранее, файл cookie маркера доступа и другие необходимые данные, полученные от API, необходимо установить в заголовках запросов для будущих запросов. Поскольку мы будем использовать Axios при отправке запросов, нам нужно настроить Axios, чтобы использовать это. В приведенном выше фрагменте мы делаем это с помощью axios.defaults.withCredentials = true
, это необходимо, потому что по умолчанию файлы cookie не передаются Axios.
aaxios.defaults.withCredentials = true
— это инструкция для Axios отправлять все запросы с учетными данными, такими как; заголовки авторизации, сертификаты клиента TLS или файлы cookie (как в нашем случае).
Мы устанавливаем наш axios.defaults.baseURL
для нашего запроса Axios к нашему API
. Таким образом, всякий раз, когда мы отправляем через Axios, он использует этот базовый URL. При этом мы можем добавлять только наши конечные точки, такие как /register
и /login
, к нашим действиям, не указывая каждый раз полный URL-адрес.
Теперь внутри папки modules
в store
создайте файл с именем 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
В нашем state
словаре мы собираемся определить наши данные и их значения по умолчанию:
const state = { user: null, posts: null, };
Мы устанавливаем значение по умолчанию state
, которое является объектом, содержащим user
и posts
с их начальными значениями как null
.
Действия
Действия — это функции, которые используются для commit
мутации для изменения состояния или могут использоваться для dispatch
, т. е. для вызова другого действия. Его можно вызывать в разных компонентах или представлениях, а затем вносить изменения в наше состояние;
Регистрация Действие
Наше действие Register
принимает данные формы, отправляет данные в нашу конечную точку /register
и присваивает ответ переменной response
. Далее мы отправим username
и password
нашей формы в действие login
в систему. Таким образом, мы входим в систему пользователя после того, как он зарегистрируется, поэтому он перенаправляется на страницу /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) },
Вход Действие
Здесь происходит основная аутентификация. Когда пользователь вводит свое имя пользователя и пароль, они передаются User
, который является объектом FormData, функция LogIn
принимает объект User
и отправляет запрос POST
к конечной точке /login
для входа пользователя.
Функция Login
, наконец, фиксирует username
в мутации setUser
.
async LogIn({commit}, User) { await axios.post('login', User) await commit('setUser', User.get('username')) },
Создать действие публикации
Наше действие CreatePost
— это функция, которая принимает post
и отправляет его в нашу конечную точку /post
, а затем отправляет действие GetPosts
. Это позволяет пользователю видеть свои сообщения после создания.
async CreatePost({dispatch}, post) { await axios.post('post', post) await dispatch('GetPosts') },
Получить сообщения действие
Наше действие GetPosts
отправляет запрос GET
в нашу конечную точку /posts
для получения сообщений в нашем API и фиксирует мутацию setPosts
.
async GetPosts({ commit }){ let response = await axios.get('posts') commit('setPosts', response.data) },
Выход из системы
async LogOut({commit}){ let user = null commit('logout', user) }
Наше действие LogOut
удаляет нашего user
из кеша браузера. Он делает это, совершая logout
из системы:
Мутации
const mutations = { setUser(state, username){ state.user = username }, setPosts(state, posts){ state.posts = posts }, LogOut(state){ state.user = null state.posts = null }, };
Каждая мутация принимает state
и значение от действия, которое ее фиксирует, кроме Logout
. Полученное значение используется для изменения определенных частей или всего, или, как в LogOut
, возвращает все переменные к нулю.
Добытчики
Геттеры — это функции для получения состояния. Его можно использовать в нескольких компонентах для получения текущего состояния. Функция isAuthenticatated
проверяет, определен ли state.user
или null, и возвращает true
или false
соответственно. StatePosts
и StateUser
возвращают state.posts
и state.user
соответственно.
const getters = { isAuthenticated: state => !!state.user, StatePosts: state => state.posts, StateUser: state => state.user, };
Теперь весь ваш файл auth.js
должен напоминать мой код на GitHub.
Настройка компонентов
1. NavBar.vue
и App.vue
В папке src/components
удалите HelloWorld.vue
и новый файл с именем NavBar.vue
.
Это компонент для нашей панели навигации, здесь перенаправляются ссылки на разные страницы нашего компонента. Каждая ссылка на маршрутизатор указывает на маршрут/страницу в нашем приложении.
v-if="isLoggedIn"
— это условие для отображения ссылки « Logout
», если пользователь вошел в систему, и скрытия маршрутов « Register
» и « Login
». У нас есть метод logout
из системы, который может быть доступен только зарегистрированным пользователям, он будет вызываться при нажатии на ссылку « Logout
». Он отправит действие LogOut
, а затем направит пользователя на страницу входа.
<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>
Теперь отредактируйте свой компонент App.vue
, чтобы он выглядел следующим образом:
<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>
Здесь мы импортировали компонент NavBar, который мы создали выше и поместили в раздел шаблона перед <router-view />
.

2. Компоненты представлений
Компоненты представлений — это разные страницы приложения, которые будут определены в рамках маршрута и доступны из панели навигации. Для начала перейдите в папку views
, удалите компонент About.vue
и добавьте следующие компоненты:
-
Home.vue
-
Register.vue
-
Login.vue
-
Posts.vue
Home.vue
Перепишите Home.vue
, чтобы он выглядел следующим образом:
<template> <div class="home"> <p>Heyyyyyy welcome to our blog, check out our posts</p> </div> </template> <script> export default { name: 'Home', components: { } } </script>
Это будет отображать приветственный текст для пользователей, когда они посещают домашнюю страницу.
Register.vue
Это страница, которую мы хотим, чтобы наши пользователи могли зарегистрироваться в нашем приложении. Когда пользователи заполняют форму, их информация отправляется в API и добавляется в базу данных, а затем выполняется вход.
Глядя на API, конечная точка /register
требует username
, full_name
имя и password
нашего пользователя. Теперь давайте создадим страницу и форму для получения этой информации:
<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>
В компоненте « Register
» нам нужно будет вызвать действие « Register
», которое получит данные формы.
<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>
Мы начинаем с импорта mapActions
из Vuex, это импортирует действия из нашего хранилища в компонент. Это позволяет нам вызывать действие из компонента.
data()
содержит локальное значение состояния, которое будет использоваться в этом компоненте, у нас есть объект form
, который содержит username
, full_name
и password
, а их начальные значения установлены в пустую строку. У нас также есть showError
, которое используется для отображения ошибки или нет.
В methods
мы импортируем действие Register
с помощью Mapactions
в компонент, поэтому действие Register
можно вызвать с помощью this.Register
.
У нас есть метод отправки, который вызывает действие Register
, к которому у нас есть доступ с помощью this.Register
, отправив ему this.form
. Если error
не возникает, мы используем this.$router
для отправки пользователя на страницу входа. В противном случае мы устанавливаем для showError
значение true.
Сделав это, мы можем включить некоторые стили.
<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
На нашей странице входа зарегистрированные пользователи вводят свое username
и password
, чтобы пройти аутентификацию с помощью API и войти на наш сайт.
<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>
Теперь нам нужно передать данные формы действию, которое отправляет запрос, а затем отправить их на защищенную 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>
Мы импортируем Mapactions
и используем его при импорте действия LogIn
в компонент, который будет использоваться в нашей функции submit
.
После действия Login
пользователь перенаправляется на страницу /posts
. В случае ошибки она перехватывается, и ShowError
устанавливается значение true.
Теперь немного стайлинга:
<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
Наша страница сообщений — это защищенная страница, доступная только для аутентифицированных пользователей. На этой странице они получают доступ к сообщениям в базе данных API. Это позволяет пользователям иметь доступ к сообщениям, а также позволяет им создавать сообщения в 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>
В приведенном выше коде у нас есть форма, в которой пользователь может создавать новые сообщения. Отправка формы должна привести к отправке сообщения в API — вскоре мы добавим метод, который делает это. У нас также есть раздел, в котором отображаются сообщения, полученные из API (если они есть у пользователя). Если у пользователя нет постов, мы просто выводим сообщение о том, что постов нет.
StateUser
и StatePosts
сопоставляются, т.е. импортируются с помощью mapGetters
в Posts.vue
а затем их можно вызывать в шаблоне.
<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>
У нас есть начальное состояние для form
, которая является объектом, имеющим title
и write_up
в качестве ключей, а значения установлены в пустую строку. Эти значения изменятся на то, что пользователь введет в форму в разделе шаблона нашего компонента.
Когда пользователь отправляет сообщение, мы вызываем метод this.CreatePost
, который получает объект формы.
Как вы можете видеть в created
жизненном цикле, у нас есть this.GetPosts
для получения сообщений при создании компонента.
Некоторая стилизация,
<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. Определение маршрутов
В нашем файле router/index.js
импортируйте наши представления и определите маршруты для каждого из них.
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. Работа с пользователями
- Неавторизованные пользователи
Если вы заметили, что при определении маршрутов наших сообщений мы добавилиmeta
-ключ, чтобы указать, что пользователь должен быть аутентифицирован, теперь нам нужен навигационный сторожrouter.BeforeEach
, который проверяет, имеет ли маршрут ключmeta: {requiresAuth: true}
. Если у маршрута естьmeta
, он проверяет хранилище на наличие токена; если он присутствует, он перенаправляет их на маршрут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
- Авторизованные пользователи
У нас также естьmeta
на маршрутах/register
и/login
.meta: {guest: true}
запрещает пользователям, вошедшим в систему, доступ к маршрутам сguest
мета.
router.beforeEach((to, from, next) => { if (to.matched.some((record) => record.meta.guest)) { if (store.getters.isAuthenticated) { next("/posts"); return; } next(); } else { next(); } });
В итоге ваш файл должен быть таким:
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. Обработка токена с истекшим сроком действия (запрещенные запросы)
Наш API настроен на истечение срока действия токенов через 30 минут, теперь, если мы попытаемся получить доступ к странице posts
через 30 минут, мы получим ошибку 401
, что означает, что нам нужно снова войти в систему, поэтому мы установим перехватчик, который читает, если мы получим Ошибка 401
, то она перенаправляет нас обратно на страницу login
.
Добавьте приведенный ниже фрагмент после объявления URL-адреса Axios по умолчанию в файле 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') } } })
Это должно привести ваш код к тому же состоянию, что и в примере на GitHub.
Заключение
Если вы смогли дойти до конца, теперь вы сможете создать полнофункциональное и безопасное интерфейсное приложение. Теперь вы узнали больше о Vuex и о том, как интегрировать его с Axios, а также о том, как сохранить его данные после перезагрузки.
Код доступен на GitHub →
Размещенный сайт:
https://nifty-hopper-1e9895.netlify.app/
API:
https://gabbyblog.herokuapp.com
Документы API:
https://gabbyblog.herokuapp.com/docs
Ресурсы
- «Обработка файлов cookie с помощью Axios», Адитья Шривастава, Medium
- «Создание средства навигации для аутентификации в Vue», Лори Барт, блог Ten Mile Square.
- «Начало работы с Vuex», официальное руководство
- «Аутентификация Vue.js JWT с помощью Vuex и Vue Router», BezKoder