做后台项目区别于做其它的项目,权限验证与安全性是非常重要的,可以说是一个后台项目一开始就必须考虑和搭建的基础核心功能。我们所要做到的是:不同的权限对应着不同的路由,同时侧边栏也需根据不同的权限,异步生成。这里先简单说一下,我实现登录和权限验证的思路。
- 登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后(我会将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态),前端会根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
- 权限验证:通过token获取用户对应的路由,通过 router.addRoutes 动态挂载这些路由。
点击登录按钮之后触发的登录操作:
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函数:
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)
})
})
},
登录时只调用登录接口,将token使用setToken方法存入cokkie中,在调用的request中,封装axios方法,使用拦截器在每次调用接口前将token放入header中,这样封装好的调用接口方法不用每次都存入token,极大的简化了我们的代码
获取用户信息:
用户登录成功之后,我们会在全局钩子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()
}
}
})
这样写可以在每次登录或者刷新页面的时候,发起获取用户信息的请求,以及路由信息的请求
- 添加路由权限】
添加路由我们是由后端来控制的,通过请求路由接口,再动态将路由表生成,在上面的代码中我们已经在permission文件中发起了请求,接下来我们要把路由表放到vuex中,再通过请求出来的信息,将返回的信息拼接成我们需要的路由。
在之前通过后端动态返回前端路由一直很难做的,因为vue-router必须是要vue在实例化之前就挂载上去的,不太方便动态改变。不过好在vue2.2.0以后新增了router.addRoutes,有了这个我们就可相对方便的做权限控制了。
以下是router/index文件和在vuex中创建的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暴露出来,我们在上面的permission文件中会将router引入,使用addRoute将得到的路由动态加载出来
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: '业务看板',
component: map['dashboardIndex'],
meta: { title: '业务看板', 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部分不能直接使用后台的信息,所以在这里使用了一个转换映射的过程,即将components的name 和 本地components 做一个映射
如:
const map={
login:require('login/index').default // 同步的方式
login:()=>import('login/index') // 异步的方式
}
//你存在服务端的map类似于
const serviceMap=[
{ path: '/login', component: 'login', hidden: true }
]
//之后遍历这个map,动态生成asyncRouterMap
//并将 component 替换为map[component]
在路由router中,我们保留了必要的基础路由,其余的路由由后台获取的数据动态添加push进router中,将路由添加好之后,通过之前的使用svg的文章,我们可以很轻松的将左侧菜单渲染出来,路由一定要放到vuex中,这样就能在路由改变的时候将数据同步渲染到页面中,在permission获取路由信息中,大量的使用了async await Promise这样的方式将信息处理异步问题,以免由于异步调用接口,使代码路由还未生成就已经执行next()方法。