Традиционно многие люди используют local storage для управления токенами, генерируемыми для аутентификации. Всегда вызывает много вопросов поиск лучшего способа управления этими токенами авторизации, таким образом что бы можно было бы хранить как можно больше информации о пользователях и при этом код был бы достаточно структурированным.
В этой статье мы расскажем как можно используя Vuex настроить авторизацию в приложение. Vuex управляет состоянием всего приложения Vue.js. Он служит централизованным хранилищем для всех компонентов приложения с заданными правилами, гарантирующими, что состояние может быть изменено только предсказуемым образом.
Если вы хотите сразу перейти к демонстрационному коду: перейдите по следующей ссылке vue-auth-vuex на GitHub
Для этого проекта мы хотим создать приложение vue с vuex и vue-router. Мы будем использовать vue cli 3.0, чтобы создать новый проект vue и выбрать опции router и vuex.
Запустите следующую команду, чтобы создать проект:
$ vue create vue-auth
Следуйте появившемуся диалоговому окну, введите необходимую информацию, выберите нужные параметры (vuex, vue-router) и завершите установку.
Далее, установите axios:
$ npm install axios --save
Нам понадобятся axios во многих наших компонентах. Давайте настроим его на начальном уровне, чтобы нам не приходилось импортировать его каждый раз, когда он нам нужен.
Откройте файл ./src/main.js
и внесите в него следующие изменения:
[...] import store from './store' import Axios from 'axios' Vue.prototype.$http = Axios; const token = localStorage.getItem('token') if (token) { Vue.prototype.$http.defaults.headers.common['Authorization'] = token } [...]
Теперь, когда мы хотим использовать axios внутри нашего компонента, мы можем использовать this.$http. Мы также установили для нашего токена заголовок Authorization в axios, чтобы наши запросы могли быть обработаны, со стороны сервера если токен потребуется. Таким образом, нам не нужно устанавливать токен каждый раз, когда мы хотим сделать запрос.
Далее, нужно настроить сервер для обработки аутентификации.
Как настроить Node сервер для обработки аутентификации я уже писал в другой статье Vue Authentication And Route Handling Using Vue-router в разделе Setup Node.js Server
Создайте файл Login.vue в каталоге ./src/components. Затем добавьте в него шаблон для страницы входа:
<template> <div> <form class="login" @submit.prevent="login"> <h1>Sign in</h1> <label>Email</label> <input required v-model="email" type="email" placeholder="Name"/> <label>Password</label> <input required v-model="password" type="password" placeholder="Password"/> <hr/> <button type="submit">Login</button> </form> </div> </template>
Далее добавьте атрибут data, которые будут привязаны к форме HTML:
[...] <script> export default { data(){ return { email : "", password : "" } }, } </script>
Теперь добавим метод для обработки входа в систему:
[...] <script> export default { [...] methods: { login: function () { let email = this.email let password = this.password this.$store.dispatch('login', { email, password }) .then(() => this.$router.push('/')) .catch(err => console.log(err)) } } } </script>
Как и компонент для входа в систему, давайте сделаем такой же для регистрации пользователей. Начните с создания файла Register.vue в каталоге компонентов:
<template> <div> <h4>Register</h4> <form @submit.prevent="register"> <label for="name">Name</label> <div> <input id="name" type="text" v-model="name" required autofocus> </div> <label for="email" >E-Mail Address</label> <div> <input id="email" type="email" v-model="email" required> </div> <label for="password">Password</label> <div> <input id="password" type="password" v-model="password" required> </div> <label for="password-confirm">Confirm Password</label> <div> <input id="password-confirm" type="password" v-model="password_confirmation" required> </div> <div> <button type="submit">Register</button> </div> </form> </div> </template>
Далее определим атрибут data, который будем связан с формой:
[...] <script> export default { data(){ return { name : "", email : "", password : "", password_confirmation : "", is_admin : null } }, } </script>
Теперь добавим метод для обработки входа в систему:
[...] <script> export default { [...] methods: { register: function () { let data = { name: this.name, email: this.email, password: this.password, is_admin: this.is_admin } this.$store.dispatch('register', data) .then(() => this.$router.push('/')) .catch(err => console.log(err)) } } } </script>
Далее создадим простой компонент, который будет отображаться только в случае аутентификации нашего пользователя. Создайте файл компонента Secure.vue и добавьте в него следующее:
<template> <div> <h1>This page is protected by auth</h1> </div> </template>
Откройте файл ./src/App.vue и добавьте в него следующее:
<template> <div id="app"> <div id="nav"> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> <span v-if="isLoggedIn"> | <a @click="logout">Logout</a></span> </div> <router-view/> </div> </template>
Можете ли вы увидеть ссылку Logout, которую мы установили, чтобы показываться только в том случае, если пользователь вошел в систему? Отлично.
Теперь, давайте добавим логику для выхода из системы:
<script> export default { computed : { isLoggedIn : function(){ return this.$store.getters.isLoggedIn} }, methods: { logout: function () { this.$store.dispatch('logout') .then(() => { this.$router.push('/login') }) } }, } </script>
Тут мы делаем две вещи — вычисляем состояние аутентификации пользователя и запускаем действие logout, когда пользователь нажимает кнопку logout. После выхода из системы мы отправляем пользователя на страницу входа с помощью this.$router.push(‘/login’).
Теперь давайте создадим модуль авторизации, используя vuex.
Во-первых, давайте настроим наш файл store.js для vuex:
import Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) export default new Vuex.Store({ state: { status: '', token: localStorage.getItem('token') || '', user : {} }, mutations: { }, actions: { }, getters : { } })
Здесь, мы импортировали vue, vuex и axios, далее определили атрибуты state. Теперь состояние vuex будет содержать наш статус аутентификации, токен jwt и информацию о пользователе.
login
Действия Vuex используются для фиксации мутаций в хранилище Vuex. Создадим действие login
, которое аутентифицирует пользователя на сервере и передает учетные данные пользователя в хранилище vuex. Откройте файл ./src/store.js и добавьте следующее к объекту действий:
... actions: { login({commit}, user){ return new Promise((resolve, reject) => { commit('auth_request') axios({url: 'http://localhost:3000/login', data: user, method: 'POST' }) .then(resp => { const token = resp.data.token const user = resp.data.user localStorage.setItem('token', token) axios.defaults.headers.common['Authorization'] = token commit('auth_success', token, user) resolve(resp) }) .catch(err => { commit('auth_error') localStorage.removeItem('token') reject(err) }) }) } } ...
Действие login передает хелпер vuex commit, который мы будем использовать для запуска мутаций.
Мы обращается к серверному API http://localhost:3000/login и возвращаем необходимые данные. Далее сохраняем токен в localStorage, и передаем токен и информацию о пользователе в auth_success для обновления атрибутов store. Также тут мы установили заголовок ‘Authorization’ для axios.
Мы могли бы хранить токен в хранилище vuex, но если пользователь покинет наше приложение, все данные в хранилище vuex исчезнут. Чтобы позволить пользователю вернуться в приложение в течение срока действия токена и не предлагать пользователю залогиниться в систему снова, мы храним токен в localStorage.
register
Как и действие login, действие register будет работать почти так же. В том же файле добавьте следующее в объект действия:
... actions: { ... register({commit}, user){ return new Promise((resolve, reject) => { commit('auth_request') axios({url: 'http://localhost:3000/register', data: user, method: 'POST' }) .then(resp => { const token = resp.data.token const user = resp.data.user localStorage.setItem('token', token) axios.defaults.headers.common['Authorization'] = token commit('auth_success', token, user) resolve(resp) }) .catch(err => { commit('auth_error', err) localStorage.removeItem('token') reject(err) }) }) } } ...
Это работает аналогично действию login, вызывая те же мутаторы и преследует одну и ту же простую цель — получить пользователя в систему.
Мы хотим, чтобы у пользователя была возможность выйти из системы, и при этом уничтожались бы все данные, созданные во время последней аутентифицированной сессии. В этот же объект действий добавьте следующее:
actions: { ... logout({commit}){ return new Promise((resolve, reject) => { commit('logout') localStorage.removeItem('token') delete axios.defaults.headers.common['Authorization'] resolve() }) } ... }
Теперь, когда пользователь щелкает, чтобы выйти, мы удаляем токен jwt, который мы сохранили вместе с установленным заголовком axios.
Мутаторы используются для изменения состояния store Vuex. Давайте определим мутаторы, которые мы использовали в нашем приложении. В объект мутаторов добавьте следующее:
mutations: { auth_request(state){ state.status = 'loading' }, auth_success(state, token, user){ state.status = 'success' state.token = token state.user = user }, auth_error(state){ state.status = 'error' }, logout(state){ state.status = '' state.token = '' }, },
Мы используем getter, чтобы получить значение атрибутов состояния vuex. Роль нашего getter в этой ситуации состоит в том, чтобы отделить данные приложения от логики приложения и обеспечить, того чтобы мы не выдавали внутреннюю информацию.
Добавьте следующие getters к объекту:
getters : { isLoggedIn: state => !!state.token, authStatus: state => state.status, }
Целью этой статьи является реализация аутентификации и защита определенных страниц от пользователя, который не является аутентификацией. Чтобы достичь этого, нам нужно знать страницу, которую пользователь хочет посетить, и в равной степени иметь возможность проверить, прошел ли пользователь аутентификацию. Нам также нужен способ сказать, зарезервирована ли страница только для аутентифицированного пользователя или не прошедшего аутентификацию пользователя, одного или обоих. Это важные соображения, которых, к счастью, мы можем достичь с помощью vue-router.
Откройте файл ./src/router.js и импортируйте то, что нам нужно для этой настройки:
import Vue from 'vue' import Router from 'vue-router' import store from './store.js' import Home from './views/Home.vue' import About from './views/About.vue' import Login from './components/Login.vue' import Secure from './components/Secure.vue' import Register from './components/Register.vue' Vue.use(Router)
Как вы можете видеть, мы импортировали vue, vue-router и нашу настройку vuex store. Мы также импортировали все определенные нами компоненты и установили vue для использования нашего маршрутизатора.
Давайте определим маршруты:
[...] let router = new Router({ mode: 'history', routes: [ { path: '/', name: 'home', component: Home }, { path: '/login', name: 'login', component: Login }, { path: '/register', name: 'register', component: Register }, { path: '/secure', name: 'secure', component: Secure, meta: { requiresAuth: true } }, { path: '/about', name: 'about', component: About } ] }) export default router
Для маршрутов, требующих аутентификации, мы добавляем к ним дополнительные данные, чтобы мы могли идентифицировать их, когда пользователь пытается получить к ним доступ. В этом суть атрибута meta
, добавленного в определение маршрута. Вы так же можете добавить еще и другие данных к атрибуту meta.
Мы определили наши маршруты. Теперь давайте проверим несанкционированный ли доступ и примем меры. В файле router.js добавьте следующее перед export default router:
router.beforeEach((to, from, next) => { if(to.matched.some(record => record.meta.requiresAuth)) { if (store.getters.isLoggedIn) { next() return } next('/login') } else { next() } })
Из статьи об использовании vue router для аутентификации вы можете вспомнить, что у нас был очень сложный механизм, который стал очень большим и запутанным. Vuex помог нам полностью упростить это, и мы можем добавить любое условие к нашему маршруту. В нашем хранилище vuex мы можем определить действия для проверки этих условий.
Поскольку мы храним наш токен в localStorage, он может оставаться там постоянно. Это означает, что всякий раз, когда мы открываем наше приложение, оно автоматически аутентифицирует пользователя, даже если срок действия токена истек. Самое худшее, что может в этом случае произойти, это то, что наши запросы продолжали бы приводит к сбою из-за неверного токена. Это плохо с точки зрения пользователя. Нужно это исправить.
Откройте файл ./src/App.vue и добавьте в него следующее:
export default { [...] created: function () { this.$http.interceptors.response.use(undefined, function (err) { return new Promise(function (resolve, reject) { if (err.status === 401 && err.config && !err.config.__isRetryRequest) { this.$store.dispatch("logout") } throw err; }); }); } }
Здесь мы перехватываем вызов axios, чтобы определить, получили ли мы ответ 401 Unauthorized. Если это так, мы запускаем действие выхода из системы, и пользователь выйдет из приложения. Это приведет его к странице входа и у него будет возможность снова войти.
В этой статье вы узнали как можно использовать хранилище vuex для управления состоянием аутентификации в приложении, используя всего несколько строк кода.
Я надеюсь, что это поможет вам создавать лучшие приложения.
Оригинальная статья: Chris Nwamba Handling Authentication In Vue Using Vuex
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
в последнем коде должно быть this.$store.dispatch("logout"), а не this.$store.dispatch(logout) (кавычек не хватает)
Спасибо, за комментарий!
так поправьте!
У меня одного не отрабатывает код из последней секции? :
created: function () {
this.$http.interceptors.response.use(undefined, function (err) {
return new Promise(function (resolve, reject) {
if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
this.$store.dispatch('logout')
}
throw err;
});
});
}
а точнее не отрабатывает только this.$store.dispatch('logout') ругается на $store
Обычно когда ругается на $store, это значит что Vuex не подключен. Точнее можно сказать, только если будет скриншет или текст ошибки. Еще можно посмотреть на комментарии в оригинальной статье (все таки это перевод).
И у меня.
Надо использовать стрелочную ф-цию здесь:
return new Promise( (resolve, reject) =>{
И стрелочную функцию в функции, которая оборачивает
this.$http.interceptors.response.use(undefined, (err) => {
return new Promise((resolve, reject) => {
if (
err.response.status === 401 &&
err.response.config &&
!err.response.config.__isRetryRequest
) {
debugger;
this.$store.dispatch("LOGOUT");
}
throw err;
});
});
Два часа времени потратил чтоб понять почему php не видит POST запрос, оказалось не хватает headers: { 'content-type': 'application/x-www-form-urlencoded' }
const Login = ({commit}, user) => {
return new Promise((resolve, reject) => {
commit('auth_request')
axios.post('https://site.ru/api/Login.php', { data: user }, { headers: { 'content-type': 'application/x-www-form-urlencoded' } })
.then(resp => {
const token = resp.data.token
const user = resp.data.user
localStorage.setItem('token', token)
axios.defaults.headers.common['Authorization'] = token
commit('auth_success', token, user)
resolve(resp)
})
.catch(err => {
commit('auth_error')
localStorage.removeItem('token')
reject(err)
})
})
}
В php получаем так $post = json_decode(file_get_contents("php://input"), true);
последнее решение с перебросом пользователя на страницу с вводом логина и пароля на мой взгляд не улучшает ux пользователя. посудите сами - зачем пользователю знать, что его токен - просрочен? он ведь не за этим пришел на сайт.
собственно вопрос: почему бы в интерсептере не сделать рефреш токена без участия пользователя ведь для этого есть все данные: просроченный токен точно принадлежит пользователю и ip места с которого была выполнена предыдущая аутентификация известно?
Согласен, только при рефреше лучше статус код о просроченности поменять (я юзаю 302 вместо 401) так ошибки в консоль при невалидном токене не вываливаются =))
Тип такого:
if (response && response.status === 302) {
if (!isAlreadyFetchingAccessToken) {
isAlreadyFetchingAccessToken = true
store.dispatch("auth/fetchAccessToken")
.then((access_token) => {
isAlreadyFetchingAccessToken = false
onAccessTokenFetched(access_token)
})
}
const retryOriginalRequest = new Promise((resolve) => {
addSubscriber(access_token => {
originalRequest.headers.Authorization = 'Bearer ' + access_token.headers.authorization
localStorage.setItem("accessToken",access_token.headers.authorization)
resolve(axios(originalRequest))
})
})
return retryOriginalRequest
}
и еще в действиях и мутациях вы вместо одного объекта передаете два значения, оно так не всегда работает. способ описанный в документации на vuex заключается в том чтобы передать один объект, в котором разместить необходимые значения.
документация https://vuex.vuejs.org/api/#vuex-store-instance-methods
Спасибо за статью.
Пожалуй лучшее описание того, как сделать аутентификацию с помощью vuex. Показали основную суть без лишних замороче, типа разделения хранилища на модули (это, конечно, хорошо, когда логика приложения разрастается, но на первых порах только усложняет понимание).
Спасибо за статью.
Не много не понял, а что будет если пользователь руками запишет какой-либо токен в localStorage?
Проверка токена происходит не на фронте, а на беке. На фронте он просто храниться.
Т.е. добавить в routers router.beforeEach - который будет сравнивать токен локальный с токеном бека?
В принципе так и сделал, не знаю насколько это бестпрактик:
if (to.matched.some(record => record.meta.requiresAuthAdmin)) {
console.log('requiresAuthAdmin')
let dataUser = {
token: localStorage.getItem('token'),
user: localStorage.getItem('user'),
};
if (!!dataUser.token) {
store.dispatch('ADMIN', dataUser)
.then((response) => {
if (response == 'ok') {
next();
return;
}
})
.catch(err => console.log(err))
} else {
next('/');
return;
}
next('/');
}
next()
не указано как подключать роутер и вьюикс в экземпляр приложения. без этого у всех ошибки будут
мне кажется те, кто этого не умеют вообще не смогут это повторить, статья подразумевает понимание многих вещей, тут к коду не подписано в каких файлах вносить изменения, новечки точно не поймут