Аутентификация в Vue.js

Опубликовано: 2022-03-10
Краткий обзор ↬ Каждое веб-приложение, которое обрабатывает пользовательские данные, должно реализовать аутентификацию. Знание того, как это сделать, важно для разработчиков Vue, и эта статья призвана пролить свет на это. Это руководство окажется полезным для начинающих разработчиков, которые хотят узнать об аутентификации в Vue. Чтобы иметь возможность следовать, вам необходимо хорошо знать Vue и Vuex.

Аутентификация — очень необходимая функция для приложений, хранящих пользовательские данные. Это процесс проверки личности пользователей, гарантирующий, что неавторизованные пользователи не смогут получить доступ к личным данным — данным, принадлежащим другим пользователям. Это приводит к ограничению маршрутов, к которым могут получить доступ только аутентифицированные пользователи. Эти аутентифицированные пользователи проверяются с использованием их регистрационных данных (т. е. имени пользователя/электронной почты и пароля) и присвоения им токена, который будет использоваться для доступа к защищенным ресурсам приложения.

В этой статье вы узнаете о:

  1. Конфигурация Vuex с Axios
  2. Определение маршрутов
  3. Работа с пользователями
  4. Обработка токена с истекшим сроком действия

Зависимости

Мы будем работать со следующими зависимостями, которые помогают в аутентификации:

  • Аксиос
    Для отправки и получения данных из нашего 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