介绍

Vue.js是用于构建前端应用程序的渐进式JavaScript框架。结合vue-router,我们可以构建具有完整动态路由的高性能应用程序。 Vue路由器是一种有效的工具,可以在我们的Vue应用程序中有效地处理身份验证。

在本教程中,我们将研究如何使用vue-router处理Vue.js应用程序不同部分的身份验证和访问控制。

步骤1 —安装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。

第2步-设置Node.js服务器

接下来,我们将设置一个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

现在已经完成所有设置,让我们创建一个可以处理用户身份验证的Node.js服务器。创建一个名为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服务器和路由器。

现在,让我们定义CORS中间件,以确保我们不会遇到任何跨源资源错误:

// 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 });
        }); 
    }); 
});

这里发生了一些事情。首先,我们将请求主体传递给数据库方法(稍后将进行定义),然后传递回调函数来处理来自数据库操作的响应。正如预期的那样,我们定义了错误检查以确保我们向用户提供准确的信息。

成功注册用户后,我们将通过电子邮件选择用户数据,并使用之前导入的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或系统上任何动态生成的端口上创建了服务器。

然后,在同一目录中创建另一个文件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

我们为数据库创建了一个类,以抽象出所需的基本功能。您可能希望在此处对数据库操作使用更通用和可重用的方法,并可能使用promise来提高效率。这将使您拥有一个可与定义的所有其他类一起使用的存储库(尤其是如果您的应用程序使用MVC架构并具有控制器)。

步骤3 —更新vue-router文件

可以在./src/router/目录中找到vue-router文件。在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() 
    }
})

export default router

Vue-router有一个beforeEach方法,该方法在处理每个路由之前被调用。在这里我们可以定义检查条件并限制用户访问权限。该方法采用三个参数-to,from和next。 to是用户希望去的地方,from是用户来自的地方,下一个是继续处理用户请求的回调函数。我们的支票已被拒收。

我们检查一下几件事:

  • 如果route requireAuth,则检查是否有jwt令牌显示用户已登录。
  • 如果route requireAuth且仅适用于管理员用户,请检查auth并检查该用户是否为管理员
  • 如果路线要求来宾,请检查用户是否已登录

我们根据要检查的内容重定向用户。我们使用路由的名称进行重定向,因此请检查以确保将其用于您的应用程序。

警告:始终确保在要检查的每个条件的末尾都有next()调用。这是为了防止在您忘记检查某种情况的情况下应用程序失败。

步骤4 —定义一些组件

为了测试我们构建的内容,让我们定义一些组件。在./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>

至此,我们已将电子邮件和密码数据属性绑定到表单字段以收集用户输入。我们向服务器发出了请求,要求对用户提供的凭据进行身份验证。

现在,让我们使用服务器的响应:

[...]
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中,以便可以从应用程序的所有部分访问它。我们将用户重定向到他们尝试访问的应用程序的任何部分,然后再重定向至登录。如果它们进入登录目录,我们将根据用户类型重定向它们。

接下来,创建一个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>

这是用户访问仪表板页面时将看到的文件。

步骤5 —全局设置Axios

对于所有服务器请求,我们将使用axios。 Axios是用于浏览器和Node.js的基于Promise的HTTP客户端。

运行以下命令以安装axios:

  • 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。现在,我们可以在this。$ http这样的所有组件中使用axios。

第6步-运行应用程序

现在我们已经完成了应用程序的构建,我们需要构建所有资产并运行它。因为我们有一个Node.js服务器以及我们的Vue应用程序,所以我们都需要它们两者才能使我们的应用程序正常工作。

让我们添加一个脚本,以帮助我们运行节点服务器。打开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

这将构建所有资产并启动应用程序。您可以打开显示您看到该应用程序的链接。

结论

在本指南中,我们使用vue-router定义了对路由的检查,并防止用户访问某些路由。我们还看到了如何基于身份验证状态将用户重定向到应用程序的不同部分。最后,我们使用Node.js构建了一个微型服务器来处理用户身份验证。

What we did is an example of how access control is designed in frameworks like Laravel. You can check out out vue-router and see what else you can do with it.