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

Код

Мы всегда стремимся получить лучший способ управления токенами авторизации, чтобы хранить еще больше информации о пользователях.

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

Похоже, лучше всегда проверять localStorage? Давайте выясним это.

Необходимый базовый уровень

  1. Знание JavaScript.
  2. js, установленный локально.
  3. Знание Vue.
  4. Установленный Vue CLI
Содержание

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

В рамках этого проекта мы создадим vue-приложение с помощью vuex и vue-router. Мы будем использовать vue cli 3.0.

Выполните следующую команду:

$ vue create vue-auth

Следуйте инструкциям, которые будут появляться в открывшемся диалоговом окне. Затем добавьте необходимую информацию, выберите нужные параметры и завершите установку.

Далее установите 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 для токена. Таким образом, нам не нужно устанавливать токен каждый раз, когда мы хотим выполнить запрос.

Настройка компонентов

Компонент аутентификации

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

После этого добавьте атрибуты данных, которые будут привязаны к 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>

Мы используем vuex-действие login для обработки аутентификации.

Компонент регистрации

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

После этого определим атрибуты данных, которые мы будем связывать с формой.

[...]
<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>

Теперь добавим логику для выхода из системы.

<script>
  export default {
    computed : {
      isLoggedIn : function(){ return this.$store.getters.isLoggedIn}
    },
    methods: {
      logout: function () {
        this.$store.dispatch('logout')
        .then(() => {
          this.$router.push('/login')
        })
      }
    },
  }
</script>

Когда пользователь кликает по ссылке для выхода из системы, мы проверяем состояние аутентификации пользователя и отправляем действие выхода из системы в хранилище vuex.

После выхода мы отправляем пользователя на страницу login с помощью this.$router.push('/login'). Вы можете изменить адрес, на который будет перенаправляться пользователь.

Модуль Vuex Auth

Мы реализуем аутентификацию с использованием 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, а затем указали vue использовать vuex. Это потому, что здесь будут выполняться сложные действия.

Мы определили атрибуты состояния. Теперь состояние vuex будет содержать статус аутентификации, токен jwt и информацию о пользователе.

Создание  Vuex-действия login

Мы создадим действие login, которое аутентифицирует пользователя на сервере и передает его учетные данные в хранилище vuex.

Откройте файл ./src/store.js и добавьте к объекту действия приведенный ниже код:

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, который мы будем использовать для запуска мутаций. Мутации вносят изменения в Vuex Store.

Мы совершаем вызов маршрута входа на сервер и возвращаем необходимые данные. Мы храним токен в localStorage, затем передаем токен и информацию о пользователе auth_success для обновления атрибутов хранилища. В этот момент также устанавливаем заголовок для axios.

Токен можно было разместить в хранилище vuex. Но если пользователь покидает приложение, все данные в хранилище vuex стираются.

Чтобы пользователь мог вернуться в приложение в течение срока действия токена, нужно хранить токен в localStorage. Мы возвращаем promise, чтобы вернуть ответ пользователю после завершения авторизации.

Создание  Vuex- действия  register

Действие register будет работать почти так же, как действие login. В том же файле добавьте приведенный ниже код в объект действия:

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. Действие register также предназначено для обработки авторизации.

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

Пользователь должен иметь возможность выйти из системы. Для этого нужно удалить все данные, созданные во время последней аутентифицированной сессии. В этот же объект actions добавьте приведенный ниже код:

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

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

Создание мутации

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

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

Мы используем геттеры, чтобы получить значение атрибутов состояния vuex. Геттеры должны отделить данные приложения от его логики.

Добавьте приведенный ниже код к объекту 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, добавленного в определение маршрута.

Обработка случаев несанкционированного доступа

Реализуем проверку несанкционированного доступа и соответствующие меры. В файле 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() 
  }
})

В хранилище 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. Если это так, то отправляем действие logout, и пользователь выйдет из приложения. Он будет перенаправлен на страницу login, на которой сможет авторизоваться повторно.

Заключение

Мы внесли значительные изменения в приложение, созданное на основе vuex. Теперь мы не полагаемся на постоянную проверку токена. Можно использовать хранилище vuex для управления состоянием аутентификации и перейти к проверке состояния в приложении, используя всего несколько строк кода.

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

Данная публикация представляет собой перевод статьи «Handling Authentication In Vue Using Vuex» , подготовленной дружной командой проекта Интернет-технологии.ру