JavaScript

Аутентификации в 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

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

Spread the love
Editorial Team

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()

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

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

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

12 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

12 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago