原文:Vue Authentication And Route Handling Using Vue-router
Vue 是一个渐进式 JavaScript 框架,它使得前端应用的构建变得简单。搭配 vue-router 就能构建拥有复杂动态路由的高性能应用。vue-router 是一个高效的工具,它能在 Vue 应用中无缝地处理用户认证。在本教程中,我们将看到如何使用 vue-router 处理用户认证和应用各部分的访问控制。
通过 Vue cli 创建一个 Vue 应用:
$ npm install -g @vue/cli
$ npm install -g @vue/cli-init
$ vue init webpack vue-router-auth
根据安装提示完成应用的安装。如果某个选项不确定,可敲 return 键(既 enter 键)输入默认值以进行下一步。当询问是否安装 vue-router 时,就确认安装。
1. 开始 - Getting Started
2. 启动 Node.js 服务 - Setup Node.js Server
3. 更新 Vue-router 文件 - Updating The Vue-router File
4. 定义组件 - Define Some Components
5. 全局安装 Axios - Setting Up Axios Globally
6. 运行应用 - Running The Application
7. 总结 - Conclusion
下一步是启动 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 的新目录,它将存放所有 Node.js 后端文件。在该目录下,创建名为 app.js
文件,其内容如下:
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 中间件,以确保不陷入任何跨域问题:
// 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', (req, res) => {
db.insert([
req.body.name,
req.body.email,
bcrypt.hashSync(req.body.password, 8)
], 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 }
)
res.status(200).send({
auth: true,
token,
user,
})
})
})
})
上述代码做了以下事情:向数据库方法(后续定义)传递了请求体(request body)和一个用于处理数据库响应的回调函数。同时,也定义了错误检查,以确保能向用户提供精确的反馈信息。
当用户成功注册时,我们通过 email 选择用户,并通过 jwt
模块为其创建用户认证 token。而 config 文件内的 secret key 则用于对用户认证进行签名。这样,我们就能验证发送到服务器的 token,避免伪造身份。
现在,定义用于注册管理员和登录的路由,两者与上述的注册逻辑类似:
router.post('/register-admin', (req, res) => {
db.insertAdmin([
req.body.name,
req.body.email,
bcrypt.hashSync(req.body.password, 8),
1,
], 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 }
)
res.status(200).send({
auth: true,
token,
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 }
)
res.status(200).send({
auth: true,
token,
user,
})
})
})
对于登录,我们使用 bcrypt 模块对哈希后的密码和用户提供的密码进行比较。若两者相同,则登录成功。反之,则登录失败,并向用户进行反馈。
通过 express 服务器让应用可响应请求:
app.use(router)
let port = process.env.PORT || 3000
let server = app.listen(port, () => {
console.log(`Express server listening on port ${port}`)
})
我们在 port: 3000
创建了一个服务,任意动态生成的端口亦可(heroku 提供 生成动态端口服务)。
然后,在同样目录下创建 config.js
文件:
module.exports = {
'secret': 'supersecret'
}
最后,创建 db.js
文件:
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 架构的应用)。
服务器端已完成开发,接下来让我们看看 Vue 应用。
该 vue-router 文件在 ./src/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 可定义元数据(meta),可基于此指定额外行为。上述代码,我们分别定义了 访客(未认证用户可见)、认证用户(认证用户可见)和管理员的路由。
基于元数据(meta)处理路由请求:
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 {
const 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 方法,其在每个路由处理前调用。可利用此特性定义检查条件并限制用户访问权限。该方法共接收 3 个参数——to、from 和 next。其中,to
是用户将去哪、from
是用户来自哪、next
是一个继续用户请求处理的回调函数。因此,检查操作将在 to
对象上进行。
我们将进行以下几点检查:
requireAuth
,则检查 jwt
token,以表明用户是否已登录。requireAuth
且要求是管理员,则检查已登录用户是否是管理员。guest
,则检查用户是否已登录。我们根据检查内容对用户请求进行重定向。由于上述代码使用路由的 name 属性进行重定向,需确保应用拥有该 name 属性的路由。
重要:始终确保检查的每个条件语句末尾均调用
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
token 和 user
信息存储到 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>
以上是当用户访问 dashboard 页面看到组件。
以上就是所需的所有组件。
对于服务端请求,我们将使用 axios。axios 是一个基于 promise 的 HTTP 库,适用于浏览器和 Node.js。使用以下命令安装 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。
至此已完成整个应用的开发。由于 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"
},
[...]
server
脚本是为了便于启动 Node.js 服务器。现在执行以下命令以启动服务器:
$ npm run server
可看到以下类似内容:
创建另一个终端实例并运行 Vue 应用:
$ npm run dev
这将会构建所有资源并启动应用。
在本教程中,我们学到了如何使用 vue-router 为路由定义检查条件,防止用户访问特定路由,也学到了如何根据认证状态进行重定向。当然,用户认证是通过我们建立的小型 Node.js 服务器进行处理。
其实,我们所做的访问控制与 Laravel 之类的框架类似。你可以看看 vue-router 还有什么有趣的事情可做。