在后台管理系统中,上一篇文章我们说到了按钮级别的权限判断,请看:GO,还有一个重要的权限判断就是菜单权限的判断,每个角色分给不同的可看菜单时必要做的事情。而且菜单全权限判断相比按钮权限判断会难实现的多。菜单权限的控制无非会有这几种思路:
1.若是角色不多,根据角色分类,例如admin,编辑,游客这样划分的话,可直接根据人员绑定角色,在前端保存路由表,路由表中有权限字段,进入系统根据人员的所属角色去匹配前端保存的路由表里面的权限(admin,编辑还是游客),从而生成可看的路由表。这种方式只适合角色比较少的情况,因为角色是固定在前端定好的。
2.第二种方式是在原来的基础上,前端只保存所有路由表,包括公共路由,异步路由,在系统我们可以选择用户所能看的菜单,然后保存权限数据在数据库。进入系统后根据前端保存的路由表与获取后端的权限数组进行匹配,从而生成可看的路由表。
其中第二种方式勾选保存的菜单时,进入系统鉴权又有两种方式:1.直接只保存权限字段数组,用这个权限字段(菜单字段)去匹配前端路由表。2.保存所有菜单数据,包括path,component路径,title,icon等等,然后再自己组装路由表数据。这种方式组装路由表数据太麻烦了,特别是后台给的是一维数组,而不是已经有层级的树形数据时,而且我做的时候遇到组装component时菜单可显示,但是点击组件却跳转不了的问题,报component为undefined。最后果断放弃了,若有知道什么原因的小伙伴可留言探讨,多谢。
组装方式为:component:() => import(`@/page/${file}/${name}.vue`)
做权限判断最有可能遇到的坑:死循环。当获取权限数据为空时,你保存在本地或者vuex中的权限数组就为空,所以当登入成功后要进入首页,判断vuex中的权限数据是否为空,为空则去拉取用户权限信息,获取的权限数据又为空,则如此死循环不停的去调接口拉取用户数据。所以在调试时一点要查看获取的权限数据,且给判断,若为空则特殊处理。
下面我们就从登入开始梳理菜单鉴权的所有逻辑思路,默认后端已经保存了权限数组(这个数组就是勾选的哪些菜单可看的字段,也可以说是菜单的name值),该数组我取的是每个路由表的name,因为保证这是唯一的即可。
login.vue:
调用登录接口,保存token,进入首页
var md5Password = self.$md5(self.loginForm.password.toUpperCase())
var para = {
userName: self.loginForm.username,
password: md5Password
}
var params = Qs.stringify(para)
console.log('查询传入参数', para)
login(params).then(res => {
console.log('登录信息', res)
if (res.token) {
self.$store.dispatch('Settoken', res.token)
} else {
// 登入成功返回的数据中没有token
self.$message.error('登录不成功,请重试...')
}
if (res.code === 1) {
self.$message({
type: 'success',
message: '登入成功'
})
// 登入成功则进入首页
self.$router.push({ path: '/dashboardPage/dashboard' })
} else if (res.code === 0) {
self.$message.error('登录不成功,请重试...')
}
})
permission.js:
在main.js中引入,菜单鉴权最重要的部分,在路由守卫中鉴权,包含了token鉴定,登入页面登入,地址栏输入地址是否可进的各种判断
import router from './router'
import store from './store'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css'// Progress 进度条样式
import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // 验权,从cookie中取得token
/** roleMenu:后台获取的菜单权限 ,Toname:要去路由的组件名称*/
function hasPermission(roleMenu, Toname) {
var commonMenu = ['dashboardPage/dashboard', '/401', '/404', '/login'] // 定义公共页面数组
if (roleMenu.indexOf(Toname) >= 0 || commonMenu.indexOf(Toname)) {
// 若是公共页面数组或者在权限路由中,页面可进
return true
} else {
return false
}
}
const whiteList = ['/login'] // 不重定向白名单
router.beforeEach((to, from, next) => { // 注册一个路由前置守卫
NProgress.start() // 进度条开始
// 拉取本地token
if (getToken()) {
console.log('totototot', to)
/* has token,有token*/
if (to.path === '/login') {
// 有token且进入的是登入页面,则直接进入首页
next({ path: '/' })
NProgress.done()
} else {
console.log('不是login进入')
/* 有token不是login页面的情况(正常的路由切换),每次页面刷新清空vuex的roleMenu后都会重新去拉取权限数据,保证是最新的权限数据*/
if (store.getters.roleMenu.length === 0) { // vuex中没有用户信息,手动输入url或者刷新浏览器,vuex数据失效情况,
console.log('无用户信息进入')
store.dispatch('GetInfoPower').then(res => { // 根据cookie的token调用接口拉取用户信息存储到vuex
if (res !== -1 && res.length > 0) {
// 实际项目拉取用户所拥有的权限
var roleMenu = res
store.dispatch('GetRouters', roleMenu).then(() => { // 根据roles权限生成可访问的路由表
router.addRoutes(store.getters.lookRouter) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
} else {
// TOKEN过期
store.dispatch('FedLogOut').then(() => {
Message.error('用户信息已过期,请重新登入')
next({ path: '/' })
})
}
}).catch((err) => {
// 拉取用户信息失败,重新登入
console.log(err)
store.dispatch('FedLogOut').then(() => {
Message.error('获取用户信息失败,请重新登入')
next({ path: '/' })
})
})
} else {
// 权限判断,防止在地址栏直接输入地址进入()
if (hasPermission(store.getters.roleMenu, to.path)) { // vuex中有用户信息,且有权限
next() // 有权限直接进入
} else {
// 无权限,进401页面
next({ path: '401' })
}
// next()
}
}
} else {
console.log('无token进入')
// 当cookie中没有token时
if (whiteList.indexOf(to.path) !== -1) { // 当前页面为值得信任的页面免验证的页面则直接进入(要进入的页面能够在白名单中找到)
next()
} else {
next({ path: '/login' })
NProgress.done()
}
}
})
router.afterEach(() => { // 后置守卫,导航被确认
NProgress.done() // 结束Progress
})
vuex部分:
import { logout, getInfo } from '@/api/login'
import { setToken, removeToken, getToken } from '@/utils/auth'
import Qs from 'qs'
const user = {
state: {
token: getToken(),
userName: '', // 用户名
powerPort: [], // 权限接口
roleMenu: [] // 后端返回的权限菜单字段数组集合
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, userName) => { // 保存用户名
state.userName = userName
},
SET_MENU: (state, roleMenu) => {
state.roleMenu = roleMenu // 有权限路由菜单(动态添加该路由)
},
SET_PORT: (state, powerPort) => { // 保存可操作接口
state.powerPort = powerPort
}
},
actions: {
// 保存token到vuex
Settoken({ commit }, token) {
setToken(token) // 存储调用API返回的token到cookie中
commit('SET_TOKEN', token) // 存储token到vuex中
},
// 获取用户信息
GetInfoPower({ commit, state }) {
return new Promise((resolve, reject) => {
var tokenObj = {
token: state.token
}
var tokenPara = Qs.stringify(tokenObj)
getInfo(tokenPara).then(response => {
var data = response.rows
console.log('权限数据', response)
if (data !== undefined && data.length > 0) {
commit('SET_NAME', response.userName)
sessionStorage.setItem('userName', response.userName)
var powerPort = [] // 可看操作接口
var roleMenu = [] // 可看权限菜单字段
// 遍历权限数组,只取code字段
for (var i = 0; i < data.length; i++) {
if (data[i].type === 0) {
powerPort.push(data[i].code)
} else {
roleMenu.push(data[i].code)
}
}
commit('SET_MENU', roleMenu)
commit('SET_PORT', powerPort) // 保存获取数据库按钮权限数组到vuex
sessionStorage.setItem('powerHandle', powerPort)
resolve(roleMenu)
} else {
resolve(response.code)
}
}).catch(error => {
reject(error)
})
})
},
// 登出
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
console.log('登出', state.token)
var tokenPara = {
token: state.token
}
var tokenParams = Qs.stringify(tokenPara)
logout(tokenParams).then(() => {
commit('SET_TOKEN', '') // 退出登入时清空vuex中的token和roles
commit('SET_MENU', [])
commit('SET_NAME', '')
console.log('登出')
removeToken() // 退出登入移除token
resolve()
}).catch(error => {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
console.log('前端登出')
resolve()
})
},
}
}
export default user
vuex中的permission.js:
根据获取的权限字段数组与本地的异步路由表进行匹配,生成可访问的路由表:
import { asyncRouterMap, constantRouterMap } from '@/router' // 引入权限路由和非权限路由
// 去前端保存的路由表中匹配路由
function findRouter(roles, route) {
if (route.name) { // 若存在路由权限表,通过路由表中的name和后端保存的权限数组进行匹配判断
return roles.indexOf(route.name) >= 0 // 检测roles中是否有在路由权限表中能找到的元素
} else {
return true
}
}
// 过滤路由表
function filterAsyncRouterInmyRouter(asyncRouterMap, roles) {
const accessedRouters = asyncRouterMap.filter(route => {
if (findRouter(roles, route)) { // 存在路由权限
if (route.children && route.children.length) { // 存在子路由
route.children = filterAsyncRouterInmyRouter(route.children, roles)
}
return true
}
return false
})
return accessedRouters
}
const permission = {
state: {
lookRouter: [], // 可访问的权限路由
AllRouter: constantRouterMap // 公共路由与异步权限路由拼接
},
mutations: {
SET_LOOKROUTERS: (state, routers) => {
state.lookRouter = routers // 可访问的权限路由
state.AllRouter = constantRouterMap.concat(routers) // 公共路由与异步权限路由拼接
}
},
actions: {
GetRouters({ commit }, data) {
return new Promise(resolve => {
var accessedRouters = filterAsyncRouterInmyRouter(asyncRouterMap, data)
var object = { path: '*', redirect: '/404', hidden: true }
accessedRouters.push(object)
commit('SET_LOOKROUTERS', accessedRouters) // 传入异步可访问的权限路由
resolve()
})
}
}
}
export default permission
vuex中的getter.js:
保存在vuexs的getter中,并
const getters = {
token: state => state.user.token,
userName: state => state.user.userName,
roleMenu: state => state.user.roleMenu, // 权限菜单字段数组
lookRouter: state => state.permission.lookRouter, // 异步权限路由数组(动态加载的路由)
AllRouter: state => state.permission.AllRouter, // 公共页面的路由数
}
export default getters
vuex的store.js文件:
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import getters from './getters''
import permission from './modules/permission'
Vue.use(Vuex)
const store = new Vuex.Store({
// 引入modules中的各个模块
modules: {
user,
permission
},
getters // 引入getters中的计算属性模块
})
export default store
在侧边栏获取并渲染权限菜单:
mapGetters 映射到本地即可
<script>
import { mapGetters } from 'vuex'
import SidebarItem from './SidebarItem'
import logo from './logo'
export default {
components: { SidebarItem, logo },
computed: {
...mapGetters([
'sidebar', // 侧边栏状态及是否有动画
'AllRouter' // 公共页面路由与异步权限路由拼接的总路由表
]),
isCollapse() { // 侧边栏展开与否
return !this.sidebar.opened
}
},
created() {
console.log('路由表', this.AllRouter)
}
}
</script>
菜单鉴权从登陆,到路由守卫的鉴权,再到获取权限数据,保存到vuex,到从获取vuex中权限数据渲染在侧边栏,注释都非常清楚。若有什么需要改进或者错误的地方,欢迎指出,谢谢。