Vue.js аутентификация и обработка маршрутов с использованием Vue-router

Spread the love

В статье продемонстрировано использование vue-router для аутентификации пользователей и контроля доступа для различных частей приложения.

Vue – это популярный Javascript фреймворк, который значительно упрощает создание веб-приложений. В сочетании с vue-router мы можем создавать высокопроизводительные приложения с динамической маршрутизацией. Vue-router – очень эффективный инструмент, который значительной упрощает реализацию аутентификации в нашем приложении Vue. В этом руководстве мы рассмотрим использование vue-router для аутентификации и контроля доступа для различных частей нашего приложения.

Начало

Для начала установим Vue cli (если Vue cli не было установлено ранее) и создадим новое приложение vue через него:

$ npm install -g @vue/cli
$ npm install -g @vue/cli-init
$ vue init webpack vue-router-auth

Следуйте инструкции по установке. Если вы не уверены в каком-либо из параметров, просто нажмите клавишу Enter, чтобы продолжить использование параметра по умолчанию. Когда вас попросят установить vue-router, примите эту опцию, потому что для этого приложения нам нужен будет именно vue-router.

Установка сервер Node.js

Далее мы настроим сервер Node.js, который будет обрабатывать аутентификацию. Сервер будем использовать SQLite в качестве базы данных. Начнем с установки драйвера SQLite:

$ npm install --save sqlite3

Поскольку мы имеем дело с паролями, нам нужно обязательно хэшировать пароли. Для этого мы будем использовать bcrypt. Запустите следующую команду, чтобы установить его:

$ npm install --save bcrypt

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

$ npm install jsonwebtoken --save

Для того чтобы прочитать данные json, которые мы будем отправлять на наш сервер, нам нужен body-parser. Так же запустите следующую команду, для его установки:

$ npm install --save body-parser

Теперь, когда все готово, давайте создадим простой сервер nodejs, который будет аутентифицировать пользователей. Создайте новый каталог с именем server. Здесь мы будем хранить все, что будем использовать для создания нашего сервера. В каталоге сервера создайте файл и сохраните его как app.js. Добавьте в этот файл следующее:

"use strict";
const express = require('express');
const DB = require('./db');
const config = require('./config');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const db = new DB("sqlitedb")
const app = express();
const router = express.Router();

router.use(bodyParser.urlencoded({ extended: false }));
router.use(bodyParser.json());

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

Теперь давайте определим CORS middleware, чтобы гарантировать, что мы не столкнемся с какими-либо ошибками доступа к ресурсам из разных источников:

// CORS middleware
const allowCrossDomain = function(req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', '*');
    res.header('Access-Control-Allow-Headers', '*');
    next();
}

app.use(allowCrossDomain)

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

Давайте определим маршрут для регистрации нового пользователя:

router.post('/register', function(req, res) {
  db.insert([
    req.body.name,
    req.body.email,
    bcrypt.hashSync(req.body.password, 8)
  ],
  function (err) {
      if (err) return res.status(500).send("There was a problem registering the user.")
      db.selectByEmail(req.body.email, (err,user) => {
        if (err) return res.status(500).send("There was a problem getting user")
        
        let token = jwt.sign(
            { 
              id: user.id 
            }, 
            config.secret, 
            {
              expiresIn: 86400 // expires in 24 hours
            });
        
        res.status(200).send(
          { 
            auth: true, 
            token: token, 
            user: user 
          });
      }); 
  }); 
});

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

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

Теперь определим два маршрута для регистрации администратора и входа в систему:

router.post('/register-admin', function(req, res) {
  db.insertAdmin([
      req.body.name,
      req.body.email,
      bcrypt.hashSync(req.body.password, 8),
      1
  ],
  function (err) {
    if (err) return res.status(500).send("There was a problem registering the user.")
    
    db.selectByEmail(req.body.email, (err,user) => {
      if (err) return res.status(500).send("There was a problem getting user")
      let token = jwt.sign({ id: user.id }, config.secret, { expiresIn: 86400 // expires in 24 hours
      });
      res.status(200).send({ auth: true, token: token, user: user });
    }); 
  }); 
});

router.post('/login', (req, res) => {
  db.selectByEmail(req.body.email, (err, user) => {
    if (err) return res.status(500).send('Error on the server.');
    
    if (!user) return res.status(404).send('No user found.');
    
    let passwordIsValid = bcrypt.compareSync(req.body.password, user.user_pass);
    
    if (!passwordIsValid) return res.status(401).send({ auth: false, token: null });
    
    let token = jwt.sign(
      { id: user.id }, 
      config.secret, 
      { expiresIn: 86400 // expires in 24 hours
      });
    
    res.status(200).send({
      auth: true,
      token: token,
      user: user
    });
  });
})

Для входа в систему мы используем bcrypt для сравнения нашего хешированного пароля с предоставленным пользователем паролем. Если они одинаковы, мы регистрируем пользователя. Если нет, отвечаем пользователю, о не успешной попытки авторизации.

Теперь используем express сервер, для запуска нашего приложения:

app.use(router)

let port = process.env.PORT || 3000;

let server = app.listen(port, function() {
    console.log('Express server listening on port ' + port)
});

Мы создали запускаем сервер на 3000 порту.

Если вас смущает любое из вышеперечисленного, и чувствуете необходимость обучения по Node.js, вы можете пройти вводный курс по Node.js.

Затем в том же каталоге, создайте другой файл config.js и добавьте в него следующее:

module.exports = {
    'secret': 'supersecret'
};

Наконец, создайте еще один файл db.js и добавьте в него следующее:

"use strict";
const sqlite3 = require('sqlite3').verbose();

class Db {
  constructor(file) {
    this.db = new sqlite3.Database(file);
    this.createTable()
  }

  createTable() {
    const sql = `
      CREATE TABLE IF NOT EXISTS user (
        id integer PRIMARY KEY, 
        name text, 
        email text UNIQUE, 
        user_pass text,
        is_admin integer)`
    return this.db.run(sql);
  }

  selectByEmail(email, callback) {
    return this.db.get(
      `SELECT * FROM user WHERE email = ?`,
      [email],function(err,row){
          callback(err,row)
      })
  }

  insertAdmin(user, callback) {
    return this.db.run(
      'INSERT INTO user (name,email,user_pass,is_admin) VALUES (?,?,?,?)',
      user, (err) => {
          callback(err)
      })
  }

  selectAll(callback) {
    return this.db.all(`SELECT * FROM user`, function(err,rows){
      callback(err,rows)
    })
  }

  insert(user, callback) {
    return this.db.run(
      'INSERT INTO user (name,email,user_pass) VALUES (?,?,?)',
      user, (err) => {
          callback(err)
      })
  }
}

module.exports = Db

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

Теперь все настроено и выглядит хорошо, давайте приступим к созданию vue приложение.

Обновление файла Vue-router

Файл vue-router можно найти в каталоге ./src/router/. В файле index.js мы определим все маршруты, которые нам необходимы для нашего приложения.

Откройте файл index.js удалите существующий код и добавьте следующее:

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Login from '@/components/Login'
import Register from '@/components/Register'
import UserBoard from '@/components/UserBoard'
import Admin from '@/components/Admin'

Vue.use(Router)

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

Теперь давайте определим маршруты для нашего приложения:

let router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/login',
      name: 'login',
      component: Login,
      meta: { 
        guest: true
      }
    },
    {
      path: '/register',
      name: 'register',
      component: Register,
      meta: { 
        guest: true
      }
    },
    {
      path: '/dashboard',
      name: 'userboard',
      component: UserBoard,
      meta: { 
        requiresAuth: true
      }
    },
    {
      path: '/admin',
      name: 'admin',
      component: Admin,
      meta: { 
        requiresAuth: true,
        is_admin : true
      }
    },
  ]
})

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

Теперь давайте обработаем запросы к этим маршрутам на основе мета-атрибутов:

router.beforeEach((to, from, next) => {
  if(to.matched.some(record => record.meta.requiresAuth)) {
    if (localStorage.getItem('jwt') == null) {
      next({
        path: '/login',
        params: { nextUrl: to.fullPath }
      })
    } else {
      let user = JSON.parse(localStorage.getItem('user'))
      if(to.matched.some(record => record.meta.is_admin)) {
        if(user.is_admin == 1){
          next()
        }
        else{
          next({ name: 'userboard'})
        }
      }else {
        next()
      }
    }
  } else if(to.matched.some(record => record.meta.guest)) {
    if(localStorage.getItem('jwt') == null){
      next()
    }
    else{
      next({ name: 'userboard'})
    }
  }else {
    next() 
  }
})

Vue-router имеет метод beforeEach, который вызывается перед обработкой каждого маршрута. Здесь мы можем определить наше условие проверки и ограничить доступ пользователей. Метод принимает три параметра – to, from и next. to это маршрут куда пользователь хочет пойти, from маршрут откуда он идет, next это функция обратного вызова, которая продолжит обработку запроса пользователя.

Мы проверяем несколько вещей:

  • если для маршрута указан requiresAuth, проверяем токен jwt, показывающий, что пользователь вошел в систему.
  • если для маршрута указан requiresAuth и оно предназначено только для пользователей с правами администратора, проверяем аутентификацию и проверяем, является ли пользователь администратором.
  • если для маршрута указан guest, проверяем, вошел ли пользователь в систему

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

ВАЖНО! Всегда проверяйте, чтобы функция next() вызывалась в конце каждого проверяемого условия. Это необходимо, так как без этого не будет перенаправление и скорее всего будет сбой приложения.

Создаем компоненты

Чтобы проверить, что мы создали, давайте определим несколько компонентов. В каталоге ./src/components/ откройте файл HelloWorld.vue и удалите существующий код и добавьте следующее:

<template>
  <div class="hello">
    <h1>This is homepage</h1>
    <h2>{{msg}}</h2>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        msg: 'Hello World!'
      }
    }
  }
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  h1, h2 {
    font-weight: normal;
  }
  ul {
    list-style-type: none;
    padding: 0;
  }
  li {
    display: inline-block;
    margin: 0 10px;
  }
  a {
    color: #42b983;
  }
</style>

Создайте новый файл Login.vue в том же каталоге и добавьте в него следующее:

<template>
  <div>
    <h4>Login</h4>
    <form>
      <label for="email" >E-Mail Address</label>
      <div>
        <input id="email" type="email" v-model="email" required autofocus>
      </div>
      <div>
        <label for="password" >Password</label>
        <div>
          <input id="password" type="password" v-model="password" required>
        </div>
      </div>
      <div>
        <button type="submit" @click="handleSubmit">
          Login
        </button>
      </div>
    </form>
  </div>
</template>

Это для шаблона HTML. Теперь давайте определим скрипт, обрабатывающий логин:

<script>
  export default {
    data(){
      return {
        email : "",
        password : ""
      }
    },
    methods : {
      handleSubmit(e){
        e.preventDefault()
        if (this.password.length > 0) {
          this.$http.post('http://localhost:3000/login', {
            email: this.email,
            password: this.password
          })
          .then(response => {

          })
          .catch(function (error) {
            console.error(error.response);
          });
        }
      }
    }
  }
</script>

На данный момент у нас есть email и password, связанные с полями формы для ввода пользовательских данных. Так же мы определили запрос к серверу для аутентификации учетных данных, которые предоставляет пользователь.

Теперь давайте обработаем ответ от сервера:

[...]
  methods:{
    handleSubmit(e){
      [...]
        .then(response => {
          let is_admin = response.data.user.is_admin
          localStorage.setItem('user',JSON.stringify(response.data.user))
          localStorage.setItem('jwt',response.data.token)

          if (localStorage.getItem('jwt') != null){
            this.$emit('loggedIn')
            if(this.$route.params.nextUrl != null){
              this.$router.push(this.$route.params.nextUrl)
            }
            else {
              if(is_admin== 1){
                this.$router.push('admin')
              }
              else {
                this.$router.push('dashboard')
              }
            }
          }
        })
       [...]
      }
    }
  }
}

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

Если вы не знакомы с созданием компонентов vue, вы можете пройти этот курс «Начало работы с Vue».

Затем создайте файл Register.vue и добавьте в него следующее:

<template>
  <div>
    <h4>Register</h4>
    <form>
      <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>

      <label for="password-confirm">Is this an administrator account?</label>
      <div>
        <select v-model="is_admin">
          <option value=1>Yes</option>
          <option value=0>No</option>
        </select>
      </div>

      <div>
        <button type="submit" @click="handleSubmit">
          Register
        </button>
      </div>
    </form>
  </div>
</template>

Теперь определим скрипт обработки регистрации:

<script>
export default {
  props : ["nextUrl"],
  data(){
    return {
      name : "",
      email : "",
      password : "",
      password_confirmation : "",
      is_admin : null
    }
  },
  methods : {
    handleSubmit(e) {
      e.preventDefault()

      if (this.password === this.password_confirmation && this.password.length > 0)
      {
        let url = "http://localhost:3000/register"
        if (this.is_admin != null || this.is_admin == 1) url = "http://localhost:3000/register-admin"
        this.$http.post(url, {
          name: this.name,
          email: this.email,
          password: this.password,
          is_admin: this.is_admin
        })
        .then(response => {
          localStorage.setItem('user', JSON.stringify(response.data.user))
          localStorage.setItem('jwt', response.data.token)

          if (localStorage.getItem('jwt') != null){
            this.$emit('loggedIn')
            if(this.$route.params.nextUrl != null){
              this.$router.push(this.$route.params.nextUrl)
            }
            else{
              this.$router.push('/')
            }
          }
        })
        .catch(error => {
          console.error(error);
        });
      } else {
        this.password = ""
        this.passwordConfirm = ""

        return alert("Passwords do not match")
      }
    }
  }
}
</script>

По структуре это код похож на файл Login.vue. Тут создается сам компонент регистрации и сопутствующий метод для обработки отправки пользователем регистрационной формы.

Теперь создайте файл Admin.vue и добавьте в него следующее:

<template>
  <div class="hello">
    <h1>Welcome to administrator page</h1>
    <h2>{{msg}}</h2>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        msg: 'The superheros'
      }
    }
  }
</script>
<style scoped>
  h1, h2 {
    font-weight: normal;
  }
  ul {
    list-style-type: none;
    padding: 0;
  }
  li {
    display: inline-block;
    margin: 0 10px;
  }
  a {
    color: #42b983;
  }
</style>

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

Наконец, создайте файл UserBoard.vue и добавьте в него следующее:

<template>
  <div class="hello">
    <h1>Welcome to regular users page</h1>
    <h2>{{msg}}</h2>
  </div>
</template>

<script>
  export default {
    data () {
      return {
          msg: 'The commoners'
      }
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  h1, h2 {
      font-weight: normal;
  }
  ul {
      list-style-type: none;
      padding: 0;
  }
  li {
      display: inline-block;
      margin: 0 10px;
  }
  a {
      color: #42b983;
  }
</style>

Это компонент, отобразиться, когда пользователь заходет на страницу панели инструментов.

И это все для компонентов.

Настройка Axios

Для всех наших запросов к серверу мы будем использовать axios. Axios – это основанный на промисах HTTP-клиент для браузера и node.js. С начало установим его следующей командой:

$ npm install --save axios

Чтобы сделать его доступным для всех наших компонентов, откройте файл ./src/main.js и добавьте следующее:

import Vue from 'vue'
import App from './App'
import router from './router'
import Axios from 'axios'

Vue.prototype.$http = Axios;

Vue.config.productionTip = false

new Vue({
    el: '#app',
    router,
    components: { App },
    template: '<App/>'
})

Определив Vue.prototype.$http = Axios, мы изменили движок vue и добавили axios. Теперь мы можем использовать axios во всех наших компонентах, как this.$http.

Запуск приложения

Теперь, когда мы закончили с приложением, нам нужно собрать все наши модули и запустить приложение. Поскольку у нас есть узел node.js сервер вместе с нашим приложением vue, запускать надо будет их обоих.

Давайте добавим скрипт, который поможет нам запустить наш сервер node.js. Откройте файл package.json и добавьте следующее:

[...]
"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "server": "node server/app",
    "build": "node build/build.js"
  },
[...]

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

$ npm run server

Вы должны увидеть что-то вроде этого:

Затем создайте другой экземпляр терминала и запустите приложение vue следующим образом:

$ npm run dev

Далее откройте ссылку http://127.0.0.1:8080/, чтобы увидеть приложение

Заключение

В этом руководстве мы продемонстрировали, как использовать vue-router для аутентификации маршрутов приложения и ограничение доступа пользователей к определенным маршрутам. Мы также показали, как перенаправлять пользователей в разные части нашего приложения на основе состояния аутентификации. Также мы создали мини-сервер с Node.js для обработки аутентификации пользователей.

Автор статьи Chris Nwamba Понравилась статья? Follow @codebeast on Twitter
Оригинал: Vue Authentication And Route Handling Using Vue-router

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

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

Я мало знаком со спа приложениями, но разве это не бредово определять роль посетителя на стороне клиента??? То есть я могу в Storage поставить 1 и вуаля, перейти по админским роутам? Да, безусловно, на стороне бэка будем проверять заголовки и не отдавать инфу, предназначенную только админам, но что на счёт фронта?

edteam
Администратор
4 лет назад
Reply to  Евгений

Вообщем то это нормально практика. Для бека должны быть свои проверки для фронта свои. То что вы сделаете на фронте никого не интересует. Если проверки на беке не пройдут данные не поменяется. А фронт полностью защитить не возможно, там всегда возможны любые изменения.

Разраб
Разраб
3 лет назад

Хорошая статья! Просим продолжение!

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

что за метод loggedIn не нашел во всем коде

Editorial Team
Администратор
3 лет назад

На странице оригинальной статьи на такой вопрос есть ответ:

“Если я правильно понимаю, это должно генерировать пользовательское событие с именем “loggedIn”. И тогда родительская страница (возможно страница App.vue?) Должна перехватить событие.
Однако я не вижу, чтобы страница App.vue ловила это событие «loggedIn» … Я предполагаю, что в данном примере это просто неиспользуемый код.

Для получения дополнительной информации об “this.$emit” – https://vuejs.org/v2/guide/components.html

Данил
Данил
3 лет назад

А как разлогиниться ?

Editorial Team
Администратор
3 лет назад
Reply to  Данил

Для этого достаточно просто удалить токен.

Данил
Данил
3 лет назад
Reply to  Editorial Team

Спасибо

Борис
Борис
2 лет назад

Добрый день! А если пользователь сделает себя админом в localStorage?

Роман
Роман
4 месяцев назад
Reply to  Борис

Очень даже ХОРОШИЙ ВОПРОС. Кто знает как пофиксить это?

Thr0TT1e
Thr0TT1e
2 лет назад

Всем привет!

А есть код компонента App.vue?

А то не понятно, что в данном компоненте написано (((