Аутентификации в Vue с использованием Vuex

Spread the love

Традиционно многие люди используют local storage для управления токенами, генерируемыми для аутентификации. Всегда вызывает много вопросов поиск лучшего способа управления этими токенами авторизации, таким образом что бы можно было бы хранить как можно больше информации о пользователях и при этом код был бы достаточно структурированным.

В этой статье мы расскажем как можно используя Vuex настроить авторизацию в приложение. Vuex управляет состоянием всего приложения Vue.js. Он служит централизованным хранилищем для всех компонентов приложения с заданными правилами, гарантирующими, что состояние может быть изменено только предсказуемым образом.

Содержание

Если вы хотите сразу перейти к демонстрационному коду: перейдите по следующей ссылке vue-auth-vuex на GitHub

Предварительные требования

  1. Знание JavaScript
  2. Установленный Node
  3. Знания Vue
  4. Установленный Vue CLI

Настройка модулей приложения

Для этого проекта мы хотим создать приложение vue с vuex и vue-router. Мы будем использовать vue cli 3.0, чтобы создать новый проект vue и выбрать опции router и vuex.

Запустите следующую команду, чтобы создать проект:

$ vue create vue-auth

Следуйте появившемуся диалоговому окну, введите необходимую информацию, выберите нужные параметры (vuex, vue-router) и завершите установку.

Далее, установите axios:

$ npm install axios --save

Настройка Axios

Нам понадобятся 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

Создайте файл 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

Как и компонент для входа в систему, давайте сделаем такой же для регистрации пользователей. Начните с создания файла 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

Далее создадим простой компонент, который будет отображаться только в случае аутентификации нашего пользователя. Создайте файл компонента Secure.vue и добавьте в него следующее:

<template>
  <div>
    <h1>This page is protected by auth</h1>
  </div>
</template>

Обновим компонент App

Откройте файл ./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.

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 и информацию о пользователе.

Создание действия Vuex 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.

Создания действия Vuex 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, вызывая те же мутаторы и преследует одну и ту же простую цель – получить пользователя в систему.

Создание действия logout

Мы хотим, чтобы у пользователя была возможность выйти из системы, и при этом уничтожались бы все данные, созданные во время последней аутентифицированной сессии. В этот же объект действий добавьте следующее:

actions: {
...
 logout({commit}){
  return new Promise((resolve, reject) => {
    commit('logout')
    localStorage.removeItem('token')
    delete axios.defaults.headers.common['Authorization']
    resolve()
  })
 }
...
}

Теперь, когда пользователь щелкает, чтобы выйти, мы удаляем токен jwt, который мы сохранили вместе с установленным заголовком axios.

Создание мутаторов (mutations)

Мутаторы используются для изменения состояния 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 = ''
  },
},

Создание геттеров (getters)

Мы используем 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

Была ли вам полезна эта статья?
[32 / 4.5]

Spread the love
Подписаться
Уведомление о
guest
24 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
йцукен
йцукен
4 лет назад

в последнем коде должно быть this.$store.dispatch(“logout”), а не this.$store.dispatch(logout) (кавычек не хватает)

Анонимно
Анонимно
2 лет назад
Reply to  Editorial Team

так поправьте!

Николай
Николай
4 лет назад

У меня одного не отрабатывает код из последней секции? :
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

Станислав
Станислав
3 лет назад
Reply to  Николай

И у меня.
Надо использовать стрелочную ф-цию здесь:
return new Promise( (resolve, reject) =>{

Valera
Valera
2 лет назад

   И стрелочную функцию в функции, которая оборачивает
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;
      });
    });

Игорь
Игорь
4 лет назад

Два часа времени потратил чтоб понять почему 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)
})
})
}

Анонимно
Анонимно
4 лет назад

В php получаем так $post = json_decode(file_get_contents(“php://input”), true);

денис
денис
4 лет назад

последнее решение с перебросом пользователя на страницу с вводом логина и пароля на мой взгляд не улучшает ux пользователя. посудите сами – зачем пользователю знать, что его токен – просрочен? он ведь не за этим пришел на сайт.
собственно вопрос: почему бы в интерсептере не сделать рефреш токена без участия пользователя ведь для этого есть все данные: просроченный токен точно принадлежит пользователю и ip места с которого была выполнена предыдущая аутентификация известно?

surtik
surtik
4 лет назад
Reply to  денис

Согласен, только при рефреше лучше статус код о просроченности поменять (я юзаю 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
}

денис
денис
4 лет назад

и еще в действиях и мутациях вы вместо одного объекта передаете два значения, оно так не всегда работает. способ описанный в документации на vuex заключается в том чтобы передать один объект, в котором разместить необходимые значения.
документация https://vuex.vuejs.org/api/#vuex-store-instance-methods

Иван Петров
Иван Петров
4 лет назад

Спасибо за статью.

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

Николай
Николай
4 лет назад

Спасибо за статью.
Не много не понял, а что будет если пользователь руками запишет какой-либо токен в localStorage?

edteam
Администратор
4 лет назад
Reply to  Николай

Проверка токена происходит не на фронте, а на беке. На фронте он просто храниться.

Николай
Николай
4 лет назад
Reply to  edteam

Т.е. добавить в routers router.beforeEach – который будет сравнивать токен локальный с токеном бека?

Николай
Николай
4 лет назад

В принципе так и сделал, не знаю насколько это бестпрактик:
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()

дмитрий
дмитрий
3 лет назад

не указано как подключать роутер и вьюикс в экземпляр приложения. без этого у всех ошибки будут

Анонимно
Анонимно
2 лет назад
Reply to  дмитрий

мне кажется те, кто этого не умеют вообще не смогут это повторить, статья подразумевает понимание многих вещей, тут к коду не подписано в каких файлах вносить изменения, новечки точно не поймут

Alona
Alona
3 лет назад

Очень неплохая статья, но не хватает информации по рефрешу токенов

Вячеслав
Вячеслав
3 лет назад

А ничего что настройки API находятся в store !? Если изменить endpoint то придется лазить во все модули store !? Нет варианта получше?

Сыглы
Сыглы
3 лет назад

Тут идёт проверка тупо на наличие токена, любой школьник заходит в консоль разработчика (, пишет в куки token ‘asdasdasd’ или пишет во вьюксе токен через ту же консоль (да, да это можно делать даже в прод. версии приложения)
document.getElementById(‘app’).__vue__.$store.state

нажимает F5 (кнопку не надо нажимать в форме логина, инча вьюкс и куки перезапишутся)
и всё, условие пройдено, привет закрытая страничка.

Johnny
Johnny
3 лет назад
Reply to  Сыглы

Ну и что? Закрытая страничка без данных это ужасная дыра безопасности? :DD
Токен, очевидно, валидируется на бэкэнде, соответственно, данные с бэка не придут. Единственный минус, что UI будет выглядеть как багованный, но простые юзеры не лазят менять непонятно что в localStorage.
Так что критика не вышла

Макс
Макс
3 лет назад

Что мы должны отправить в Api и что должны получить?