接上篇,这次是真的接上篇,针对上篇未完成的部分,增加鉴权功能,开始之前,我们先要介绍一个新的知识,路由元数据。
在vue-router中,定义元数据的方式:
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, children: [ { path: 'bar', component: Bar, // a meta field meta: { requiresAuth: true } } ] } ] })
那么如何访问这个 meta
字段呢?
首先,我们把routes
配置中的每个路由对象叫做路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录
例如,根据上面的路由配置,/foo/bar
这个 URL 将会匹配父路由记录以及子路由记录。
一个路由匹配到的所有路由记录会暴露为 $route
对象(还有在导航钩子中的 route 对象)的 $route.matched
数组。因此,我们需要遍历 $route.matched
来检查路由记录中的 meta
字段。
所以在vue-router官方文档中,我们可以看到下面的代码,其实就是前端路由授权的粗糙实现方式(代码不做过多解释,里面我加入了详细的注释):
router.beforeEach((to, from, next) => { if (to.matched.some(record => record.meta.requiresAuth)) { // 如果路由配置了元数据requiresAuth为true,则需要鉴权,这是需要判断是否登录 // 如果没有登录则跳转到login页面 if (!auth.loggedIn()) { next({ path: '/login', //这里传递fullPath,是为了登录之后作为return back query: { redirect: to.fullPath } }) } else { //如果已经登录过,直接执行进入下一步 next() } } else { //对没有配置requiresAuth的路由进行处理,如果不加入,则路由未配置requiresAuth,无法进入,所以确保一定要调用 next() next() } })
好了,基础知识介绍完毕,现在我们把我们的路由加入meta信息,启用权限验证:
var router = new VueRouter({ routes: [{ name: 'home', path: '/home', component: HomeComponent }, { name: 'customers', path: '/customers', component: CustomerListComponent, meta: { auth: true } }, { name: 'detail', path: '/detail/:id', component: CustomerComponent, meta: { auth: true } }, { name: 'login', path: '/login', component: LoginComponent } ] });
//注册全局事件钩子 router.beforeEach(function (to, from, next) { //如果路由中配置了meta auth信息,则需要判断用户是否登录; if (to.matched.some(r => r.meta.auth)) { //登录后会把token作为登录的标示,存在localStorage中 if (!localStorage.getItem('token')) { console.log("需要登录"); next({ path: '/login', query: { to: to.fullPath } }) } else { next(); } } else { next() } });
更新代码后,可以跟目录运行node app.js ,打开8110端口,查看,运行效果如下:
这个时候,无论从浏览器地址栏还是通过跳转方式,在点击配置了 meta:{auth:true}的路由时,如果没有登录,都会跳转到登录页面,并记录return back url。
下面我们加入登录逻辑,并修改后台接口,支持用户授权,后台我们使用jwt的一个实现https://github.com/auth0/node-jsonwebtoken ,直接使用npm 安装即可,对jwt不太了解的同学,可以搜索 json web token (jwt)(另外为了读取http body,我们这里会使用 body-parser,可以直接使用npm install --save body-parser 安装)。
首先修改我们的登录组件:
methods: { login: function () { var self = this; axios.post('/login', this.user) .then(function (res) { console.log(res); if (res.data.success) { localStorage.setItem('token', res.data.token); console.log(self.$router); self.$router.push({ path: self.$route.query.to }); } else { alert(res.data.errorMessage); } }) .catch(function (error) { console.log(error); }); } }
并添加全局拦截器,在任何ajax请求中加入token 头,如果熟悉angular拦截器的同学对axios实现的拦截器应该很熟悉的,这和jquery 对Ajax.setting的设置类似:
// request 拦截器 ,对所有请求,加入auth axios.interceptors.request.use( cfg => { // 判断是否存在token,如果存在,则加上token if (localStorage.getItem('token')) { cfg.headers.Authorization = localStorage.getItem('token'); } return cfg; }, err => { return Promise.reject(err); }); // http response 拦截器 axios.interceptors.response.use( res => { return res; }, err => { if (err.response) { switch (err.response.status) { case 401: //如果未授权访问,则跳转到登录页面 router.replace({ path: '/login', query: {redirect: router.currentRoute.fullPath} }) } } return Promise.reject(err.response.data) });
这样,我们再每次请求的时候,如果token存在,则就会带上token;
接着,修改我们的后端部分,加入处理登录,以及生成解析token的部分,修改我们的authMiddleware.js文件:
var jwt = require('jsonwebtoken'); /** * 有效用户列表 */ var validUsers = [{ username: 'zhangsan', password: '123456' }, { username: 'lisi', password: '123456' }]; //FIXME:这个作为密钥,一定要安全的,这里我为了简单就直接写了一大段字符串 const secretKey = 'dhahr3uiudfu93u43i3uy43&*&$#*&437hjhfjdjhfdfjsy8&*&*JNFSJDJHH??>:LP'; /** * 创建token * @param {用户对象} user */ var createToken = function (user) { /** * 创建token 并设置过期时间为一个小时 */ return jwt.sign({ data: user, exp: Math.floor(Date.now() / 1000) + (60 * 60) }, secretKey); } /** * 解析token * @param {用户需要验证的token} token */ var parseToken = function (token, callback) { jwt.verify(token, secretKey, function (err, result) { callback && callback(err, result); }); } module.exports = function (req, res, next) { //如果是登录请求 console.log(req.path); if (req.path === "/login") { var username = req.body.username; var password = req.body.password; //判断用户名和密码是否正确 var user = validUsers.filter(u => u.username === username && u.password === password)[0]; //如果用户用户名密码匹配成功,直接创建token并返回 if (user) { res.json({ success: true, token: createToken(user) }) } else { res.json({ success: false, errorMessage: 'username or password is not correct,please retry again' }) } } else {//如果不是登录请求,则需要检查token 的合法性 var token = req.get('Authorization'); console.log(token); if (token) { parseToken(token, function (err, result) { if (err) {//如果解析失败,则返回失败信息 res.status(401).json( { success: false, errorMessage: JSON.stringify(err) }) } else { next(); } }) }else{ res.status(401).json({ success:false, errorMessage:'未授权的访问' }); } } }
上述代码加上注释应该没什么复杂度的,各位同学应该可以看的明白,这样之后,我们启用我们的授权中间件,修改/app.js文件:
var express = require("express"); var bodyParser = require("body-parser"); var authMiddleware = require('./middleware/authMiddleware'); var customerRouter = require('./router/customers'); var app = express(); app.use(express.static('public')); app.get('/portal', function (req, res) { res.json({ data: [ { visits: 12, clicks: 100 }, { location: 'BeiJing', total: 17 } ] }) }) app.use(bodyParser.json()) app.use(authMiddleware); app.use('/api', customerRouter);
运行我们的代码可以看到如下效果:
博客园对图片大小有要求,不能很好的截取,就只截取了一部分,这是登录后的效果,登录前的效果,大家可以自己测试,完整代码如下:
/app.js
var express = require("express"); var bodyParser = require("body-parser"); var authMiddleware = require('./middleware/authMiddleware'); var customerRouter = require('./router/customers'); var app = express(); app.use(express.static('public')); app.get('/portal', function (req, res) { res.json({ data: [ { visits: 12, clicks: 100 }, { location: 'BeiJing', total: 17 } ] }) }) app.use(bodyParser.json()) app.use(authMiddleware); app.use('/api', customerRouter); app.listen(8110, function () { console.log("port 8110 is listenning!!!"); });
/public/app.js
var LoginComponent = { template: ` <div class="login" > username:<input type="text" v-model="user.username" /> password:<input type="password" v-model="user.password" /> <input type="button" @click="login()" value="login" /> </div> `, data: function () { return { user: { username: '', password: '' } } }, methods: { login: function () { var self = this; axios.post('/login', this.user) .then(function (res) { console.log(res); if (res.data.success) { localStorage.setItem('token', res.data.token); console.log(self.$router); self.$router.push({ path: self.$route.query.to }); } else { alert(res.data.errorMessage); } }) .catch(function (error) { console.log(error); }); } } } var CustomerListComponent = { template: ` <div> <div> <input type="text" v-model="keyword" /> <input type="button" @click="getCustomers()" value="search" /> </div> <ul> <router-link v-for="c in customers" tag="li" :to="{name:'detail',params:{id:c.id}}" :key="c.id">{{c.name}}</router-link> </ul> </div> `, data: function () { return { customers: [], keyword: '' } }, created: function () { this.getCustomers(); }, methods: { getCustomers: function () { axios.get('/api/getCustomers', { params: { keyword: this.keyword } }) .then(res => { this.customers = res.data; console.log(res) }) .catch(err => console.log(err)); }, } } var CustomerComponent = { template: ` <div> {{customer}} </div> `, data: function () { return { customer: {} } }, created: function () { var id = this.$route.params.id; this.getCustomerById(id); }, watch: { '$route': function () { console.log(this.$route.params.id); } }, methods: { getCustomerById: function (id) { axios.get('/api/customer/' + id) .then(res => this.customer = res.data) .catch(err => console.log(err)); } } } var HomeComponent = { template: `<div> <h1>Home 页面,portal页</h1> <h2>以下数据来自服务端</h2> {{stat}} </div>`, data: function () { return { stat: ''//代表相关统计信息等 } }, methods: { getStat: function () { return axios.get('/portal'); } }, created: function () { this.getStat().then(res => { this.stat = JSON.stringify(res.data); }).catch(err => { console.log(err); }) } } var router = new VueRouter({ routes: [{ name: 'home', path: '/home', component: HomeComponent }, { name: 'customers', path: '/customers', component: CustomerListComponent, meta: { auth: true } }, { name: 'detail', path: '/detail/:id', component: CustomerComponent, meta: { auth: true } }, { name: 'login', path: '/login', component: LoginComponent } ] }); //注册全局事件钩子 router.beforeEach(function (to, from, next) { //如果路由中配置了meta auth信息,则需要判断用户是否登录; if (to.matched.some(r => r.meta.auth)) { //登录后会把token作为登录的标示,存在localStorage中 if (!localStorage.getItem('token')) { console.log("需要登录"); next({ path: '/login', query: { to: to.fullPath } }) } else { next(); } } else { next() } }); // request 拦截器 ,对所有请求,加入auth axios.interceptors.request.use( cfg => { // 判断是否存在token,如果存在,则加上token if (localStorage.getItem('token')) { cfg.headers.Authorization = localStorage.getItem('token'); } return cfg; }, err => { return Promise.reject(err); }); // http response 拦截器 axios.interceptors.response.use( res => { return res; }, err => { if (err.response) { switch (err.response.status) { case 401: //如果未授权访问,则跳转到登录页面 router.replace({ path: '/login', query: {redirect: router.currentRoute.fullPath} }) } } return Promise.reject(err.response.data) }); var app = new Vue({ router: router, template: ` <div> <router-link :to="{name:'home'}" >Home</router-link> <router-link :to="{name:'customers'}" >Customers</router-link> <router-view></router-view> </div> `, el: '#app' });
/public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>demo3</title> <script src="https://cdn.bootcss.com/vue/2.4.1/vue.js"></script> <script src="https://cdn.bootcss.com/vue-router/2.7.0/vue-router.js"></script> <script src="https://cdn.bootcss.com/axios/0.16.2/axios.js"></script> </head> <body> <div id="app"> </div> <script src="./app.js"></script> </body> </html>
/router/customers.js
var router = require("express").Router(); var db = require('./fakeData'); router.get('/getCustomers', function (req, res) { var list = db.data; list = list.filter(v => v.name.indexOf(req.query.keyword) !== -1); res.json(list); }); router.get('/customer/:id',function(req,res){ var list=db.data; var obj=list.filter(v=>v.id==req.params.id)[0]; res.json(obj); }) module.exports = router;
/router/fakeData.json
{ "data": [ { "id":1, "name": "zhangsan", "age": 14, "sexy": "男", "majar": "学生" }, { "id":2, "name": "lisi", "age": 19, "sexy": "女", "majar": "学生" }, { "id":3, "name": "wangwu", "age": 42, "sexy": "男", "majar": "工人" }, { "id":4, "name": "maliu", "age": 10, "sexy": "男", "majar": "学生" }, { "id":5, "name": "wangermazi", "age": 82, "sexy": "男", "majar": "画家" }, { "id":6, "name": "liudehua", "age": 55, "sexy": "男", "majar": "天王" }, { "id":7, "name": "zhoujielun", "age": 14, "sexy": "男", "majar": "歌手" }, { "id":8, "name": "wangfei", "age": 50, "sexy": "女", "majar": "歌手" }, { "id":9, "name": "mayun", "age": 60, "sexy": "男", "majar": "老板" } ] }
/middleware/authMiddleware.js
var jwt = require('jsonwebtoken'); /** * 有效用户列表 */ var validUsers = [{ username: 'zhangsan', password: '123456' }, { username: 'lisi', password: '123456' }]; //FIXME:这个作为密钥,一定要安全的,这里我为了简单就直接写了一大段字符串 const secretKey = 'dhahr3uiudfu93u43i3uy43&*&$#*&437hjhfjdjhfdfjsy8&*&*JNFSJDJHH??>:LP'; /** * 创建token * @param {用户对象} user */ var createToken = function (user) { /** * 创建token 并设置过期时间为一个小时 */ return jwt.sign({ data: user, exp: Math.floor(Date.now() / 1000) + (60 * 60) }, secretKey); } /** * 解析token * @param {用户需要验证的token} token */ var parseToken = function (token, callback) { jwt.verify(token, secretKey, function (err, result) { callback && callback(err, result); }); } module.exports = function (req, res, next) { //如果是登录请求 console.log(req.path); if (req.path === "/login") { var username = req.body.username; var password = req.body.password; //判断用户名和密码是否正确 var user = validUsers.filter(u => u.username === username && u.password === password)[0]; //如果用户用户名密码匹配成功,直接创建token并返回 if (user) { res.json({ success: true, token: createToken(user) }) } else { res.json({ success: false, errorMessage: 'username or password is not correct,please retry again' }) } } else {//如果不是登录请求,则需要检查token 的合法性 var token = req.get('Authorization'); console.log(token); if (token) { parseToken(token, function (err, result) { if (err) {//如果解析失败,则返回失败信息 res.status(401).json( { success: false, errorMessage: JSON.stringify(err) }) } else { next(); } }) }else{ res.status(401).json({ success:false, errorMessage:'未授权的访问' }); } } }
/package.json
{ "name": "vue_demo3", "version": "1.0.0", "description": "", "main": "app.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "body-parser": "^1.17.2", "express": "^4.15.3", "jsonwebtoken": "^7.4.1" } }