做后臺(tái)項(xiàng)目區(qū)別于做其它的項(xiàng)目,權(quán)限驗(yàn)證與安全性是非常重要的,可以說是一個(gè)后臺(tái)項(xiàng)目一開始就必須考慮和搭建的基礎(chǔ)核心功能。我們所要做到的是:不同的權(quán)限對(duì)應(yīng)著不同的路由,同時(shí)側(cè)邊欄也需根據(jù)不同的權(quán)限,異步生成。這里先簡(jiǎn)單說一下,我實(shí)現(xiàn)登錄和權(quán)限驗(yàn)證的思路。
- 登錄:當(dāng)用戶填寫完賬號(hào)和密碼后向服務(wù)端驗(yàn)證是否正確,驗(yàn)證通過之后,服務(wù)端會(huì)返回一個(gè)token,拿到token之后(我會(huì)將這個(gè)token存貯到cookie中,保證刷新頁(yè)面后能記住用戶登錄狀態(tài)),前端會(huì)根據(jù)token再去拉取一個(gè) user_info 的接口來獲取用戶的詳細(xì)信息(如用戶權(quán)限,用戶名等等信息)。
- 權(quán)限驗(yàn)證:通過token獲取用戶對(duì)應(yīng)的路由,通過 router.addRoutes 動(dòng)態(tài)掛載這些路由。
點(diǎn)擊登錄按鈕之后觸發(fā)的登錄操作:
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.$store.dispatch('user/login', this.loginForm).then((res) => {
this.loading = false
this.$router.push({ path: this.redirect || '/' })
}).catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
login函數(shù):
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
commit('SET_TOKEN', response.token)
setToken(response.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
登錄時(shí)只調(diào)用登錄接口,將token使用setToken方法存入cokkie中,在調(diào)用的request中,封裝axios方法,使用攔截器在每次調(diào)用接口前將token放入header中,這樣封裝好的調(diào)用接口方法不用每次都存入token,極大的簡(jiǎn)化了我們的代碼
獲取用戶信息:
用戶登錄成功之后,我們會(huì)在全局鉤子router.beforeEach中攔截路由,判斷是否已獲得token,在獲得token之后我們就要去獲取用戶的基本信息了
在permission文件中:
router.beforeEach(async(to, from, next) => {
NProgress.start()
document.title = getPageTitle(to.meta.title)
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.name
if (hasGetUserInfo) {
next()
} else {
try {
await store.dispatch('user/getInfo')
const accessRoute = await store.dispatch('router/getSysRouter')
router.addRoutes(accessRoute)
next({ ...to, replace: true })
} catch (error) {
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
這樣寫可以在每次登錄或者刷新頁(yè)面的時(shí)候,發(fā)起獲取用戶信息的請(qǐng)求,以及路由信息的請(qǐng)求
- 添加路由權(quán)限】
添加路由我們是由后端來控制的,通過請(qǐng)求路由接口,再動(dòng)態(tài)將路由表生成,在上面的代碼中我們已經(jīng)在permission文件中發(fā)起了請(qǐng)求,接下來我們要把路由表放到vuex中,再通過請(qǐng)求出來的信息,將返回的信息拼接成我們需要的路由。
在之前通過后端動(dòng)態(tài)返回前端路由一直很難做的,因?yàn)関ue-router必須是要vue在實(shí)例化之前就掛載上去的,不太方便動(dòng)態(tài)改變。不過好在vue2.2.0以后新增了router.addRoutes,有了這個(gè)我們就可相對(duì)方便的做權(quán)限控制了。
以下是router/index文件和在vuex中創(chuàng)建的router文件:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import menuModule from '@/store/modules/router'
const createRouter = () => new Router({
scrollBehavior: () => ({ y: 0 }),
routes: menuModule.state.router
})
const router = createRouter()
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
將router暴露出來,我們?cè)谏厦娴膒ermission文件中會(huì)將router引入,使用addRoute將得到的路由動(dòng)態(tài)加載出來
import { reqGet } from '@/api/httpReq'
const map = {
layout: () => import('@/layout'),
dashboardIndex: () => import('@/views/dashboard/index'),
doBusinessIndex: () => import('@/views/doBusiness/index'),
doBusinessAgentDetail: () => import('@/views/doBusiness/agentDetail'),
carSearchIndex: () => import('@/views/carSearch/index'),
carSearchDetail: () => import('@/views/carSearch/detail'),
smartCarbetIndex: () => import('@/views/smartCarbet/index'),
alarmSearchIndex: () => import('@/views/alarmSearch/index'),
alarmSearchDetail: () => import('@/views/alarmSearch/detail'),
tagSearchIndex: () => import('@/views/tagSearch/index')
}
const state = {
router: [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: map['layout'],
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: '業(yè)務(wù)看板',
component: map['dashboardIndex'],
meta: { title: '業(yè)務(wù)看板', icon: 'yingyongguanli' }
}]
}
]
}
const mutations = {
pushRouterIn(state, data) {
state.router.push(data)
}
}
const actions = {
getSysRouter(context) {
return new Promise(resolve => {
reqGet('/sys/menu/list', 'get').then(res => {
var arr = res.menuList
var asyncRoute = initRouter(arr, context.state.router)
context.state.router = asyncRoute
resolve(asyncRoute)
})
})
}
}
function initRouter(arr, router) {
var arr2 = router
for (let i in arr) {
let c1 = {}
let a1 = arr[i]
c1.meta = a1.meta
c1.name = a1.name
c1.path = a1.path
c1.redirect = a1.redirect
c1.component = map[a1.component]
c1.children = []
if (a1.children.length) {
for (let n in a1.children) {
let a2 = a1.children[n]
let c2 = {}
if (a2.meta.length) {
c2.meta = a2.meta
}
c2.path = a2.path
c2.name = a2.name
c2.component = map[a2.component]
c2.hidden = true
c1.children.push(c2)
}
}
arr2.push(c1)
}
return arr2
}
export default {
namespaced: true,
state,
mutations,
actions
}
由于獲取的路由信息為字符串,前端引入的component部分不能直接使用后臺(tái)的信息,所以在這里使用了一個(gè)轉(zhuǎn)換映射的過程,即將components的name 和 本地components 做一個(gè)映射
如:
const map={
login:require('login/index').default // 同步的方式
login:()=>import('login/index') // 異步的方式
}
//你存在服務(wù)端的map類似于
const serviceMap=[
{ path: '/login', component: 'login', hidden: true }
]
//之后遍歷這個(gè)map,動(dòng)態(tài)生成asyncRouterMap
//并將 component 替換為map[component]
在路由router中,我們保留了必要的基礎(chǔ)路由,其余的路由由后臺(tái)獲取的數(shù)據(jù)動(dòng)態(tài)添加push進(jìn)router中,將路由添加好之后,通過之前的使用svg的文章,我們可以很輕松的將左側(cè)菜單渲染出來,路由一定要放到vuex中,這樣就能在路由改變的時(shí)候?qū)?shù)據(jù)同步渲染到頁(yè)面中,在permission獲取路由信息中,大量的使用了async await Promise這樣的方式將信息處理異步問題,以免由于異步調(diào)用接口,使代碼路由還未生成就已經(jīng)執(zhí)行next()方法。