接上篇,這次是真的接上篇,針對上篇未完成的部分,增加鑒權功能,開始之前,我們先要介紹一個新的知識,路由元數據。
在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" } }
