前言
vue-admin-template是一个最基础的后台管理模板,只包含了一个后台需要最基础的东西,如果clone
的是它的master分支
,是没有权限管理的,只有完整版vue-element-admin
有这个功能,但是为了小小的一个权限管理而用比较复杂的有点得不偿失。
我在网上找了一堆教程和资料,发现要么说的很乱,要么说的不全,最后连个完整代码都不让我白嫖(bushi)。自己复制粘贴过去都实现不出来,仔细查看发现人家写的教程漏了一写步骤/代码,而且还有bug(服了这些老六)。
在自己摸索了和看了花裤衩大佬的文章后,解决了一些bug自己实现出来了,代码中也有详细注释。完整代码放文末给大家了,大家记得给我star再走(不然小拳拳锤你胸口)。
权限管理?动态路由?
现在开发后台管理系统项目经常有权限管理的需求,权限管理其实就是根据不同的角色权限显示不同的路由,而其中的关键就是动态路由router.addRoutes
实现权限验证的基本思路就是:
- 用户登录,通过token获取用户对应的 role
- 动态根据用户的 role 算出其对应有权限的路由
- 通过
router.addRoutes
动态挂载这些路由
以上步骤实现的核心是router和vuex,下面就详细介绍如何实现(按代码执行逻辑倒推)
具体实现
- 创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。
- 当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。
- 调用
router.addRoutes(store.getters.addRouters)
添加用户可访问的路由。 - 使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件
本段转载自vue-element-admin的作者:花裤衩
1.router.js路由表
首先我们实现router.js路由表,
一共有两个路由表,
- 一个是
constantRoutes
,这个用来放没有权限要求的页面,每个角色都可以访问,比如首页,登录页; - 一个是
asyncRoutes
动态需要根据权限加载的路由表 。meta
里面的roles
就存放了页面需要的权限,这些页面只有roles数组里面的角色才能看到。
注意:404一定要放最后面,不然都会重定向到404
src/router/index.js
代码如下:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* constantRoutes
* 没有权限要求的基本页面
*所有角色都可以访问
如首页和登录页和一些不用权限的公用页面
*/
export const constantRoutes = [{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: {
title: 'Dashboard',
icon: 'dashboard',
}
}]
},
]
//异步挂载的路由
//动态需要根据权限加载的路由表
export const asyncRoutes = [{
path: '/example',
component: Layout,
redirect: '/example/table',
name: 'Example',
alwaysShow: true,
meta: {
title: 'Example',
icon: 'el-icon-s-help',
},
children: [{
path: 'table',
name: 'Table',
component: () => import('@/views/table/index'),
meta: {
title: 'Table',
icon: 'table',
roles: ['editor']
}
},
{
path: 'tree',
name: 'Tree',
component: () => import('@/views/tree/index'),
meta: {
title: 'Tree',
icon: 'tree',
roles: ['admin', 'editor']
}
}
]
},
{
path: '/form',
component: Layout,
children: [{
path: 'index',
name: 'Form',
component: () => import('@/views/form/index'),
meta: {
title: 'Form',
icon: 'form',
roles: ['editor']
}
}]
},
{
path: '/nested',
component: Layout,
redirect: '/nested/menu1',
alwaysShow: true,
name: 'Nested',
meta: {
title: 'Nested',
icon: 'nested',
},
children: [{
path: 'menu1',
component: () => import('@/views/nested/menu1/index'), // Parent router-view
name: 'Menu1',
meta: {
title: 'Menu1',
roles: ['admin']
},
children: [{
path: 'menu1-1',
component: () => import('@/views/nested/menu1/menu1-1'),
name: 'Menu1-1',
meta: {
title: 'Menu1-1'
}
},
{
path: 'menu1-2',
component: () => import('@/views/nested/menu1/menu1-2'),
name: 'Menu1-2',
meta: {
title: 'Menu1-2'
},
children: [{
path: 'menu1-2-1',
component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
name: 'Menu1-2-1',
meta: {
title: 'Menu1-2-1'
}
},
{
path: 'menu1-2-2',
component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
name: 'Menu1-2-2',
meta: {
title: 'Menu1-2-2'
}
}
]
},
{
path: 'menu1-3',
component: () => import('@/views/nested/menu1/menu1-3'),
name: 'Menu1-3',
meta: {
title: 'Menu1-3'
}
}
]
},
{
path: 'menu2',
component: () => import('@/views/nested/menu2/index'),
name: 'Menu2',
meta: {
title: 'menu2',
roles: ['editor']
}
}
]
},
// 如果需要配置重定向404页面的话,需要配置在asyncRoutes的最后
{
path: '*',
redirect: '/404',
hidden: true
}
]
// 实例化vue的时候只挂载constantRouter
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({
y: 0
}),
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
2. src/permission.js动态添加路由
我们在登录成功后,router
会重定向一个新页面,在跳转之前src/permission.js
里面路由守卫router.beforeEach
会先做一些拦截验证,根据判断会做不同页面跳转和操作,比如没登录就先跳到登录页,根据获取到的用户信息roles筛选路由后动态添加路由。
src/permisson.js
具体实现代码与注释:
import router from './router'
import store from './store'
import {
Message
} from 'element-ui'
// 页面进度条组件
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import {
getToken
} from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({
showSpinner: false
}) // NProgress 配置
const whiteList = ['/login', '/404'] // 不重定向的白名单
router.beforeEach(async (to, from, next) => {
// start progress bar
NProgress.start()
// 设置页面标题
document.title = getPageTitle(to.meta.title)
// 确定用户是否已登录
const hasToken = getToken()
// 判断是否存在token,没有就重新登陆
if (hasToken) {
if (to.path === '/login') {
// 如果已登录,则重定向到主页
next({
path: '/'
})
NProgress.done()
} else {
// 确定用户是否通过getInfo获得了权限角色
const hasRoles = store.getters.roles && store.getters.roles.length > 0 //这里指的是src/store/getters.js的roles
// console.log(hasRoles)
//判断是否已经有roles了
if (hasRoles) {
next(); //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面
} else {
try {
// get user info
// 注意: roles 角色必须是对象数组! 例如: ['admin'] 或 ,['developer','editor']
// 1. 获取roles
const {
roles
} = await store.dispatch('user/getInfo') //第一步
// 2. 根据角色生成可访问路由图
// 获取通过权限验证的路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles) //第二步
// 3. 更新加载路由
router.options.routes = store.getters.permission_routes //第三步
// 动态添加可访问路由
router.addRoutes(accessRoutes)
// console.log(store)
// console.log(accessRoutes);
// hack方法 确保addRoutes已完成,以确保地址是完整的
// 设置replace: true,这样导航就不会留下历史记录
next({
...to,
replace: true
})
} catch (error) {
// 删除token并转到登录页面重新登录
await store.dispatch('user/resetToken')
Message.error('出现错误~请重新登录')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* 没有token */
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
NProgress.done()
}
}
})
3. store/modules/user.js
而获取+存储roles通过store/modules/user.js
实现,筛选+存储路由又是通过通过store/modules/permission.js
实现的
store/modules/user.js
主要是获取和存储roles
store/modules/user.js
完整代码:
import {
login,
logout,
getInfo
} from '@/api/user'
import {
getToken,
setToken,
removeToken
} from '@/utils/auth'
import {
resetRouter
} from '@/router'
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: '',
roles: [],
}
}
const state = getDefaultState()
const mutations = {
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
}
}
const actions = {
// user login
login({
commit
}, userInfo) {
const {
username,
password
} = userInfo
return new Promise((resolve, reject) => {
login({
username: username.trim(),
password: password
}).then(response => {
const {
data
} = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// get user info
getInfo({
commit,
state
}) {
return new Promise((resolve, reject) => {
// state.token之前没有传 出现了重复登陆问题
getInfo(state.token).then(response => {
const {
data
} = response
if (!data) {
return reject('验证失败,请重新登录')
}
const {
name,
roles,
avatar
} = data
if (!roles || roles.length <= 0) {
reject('getInfo:roles must be a non-null array!')
}
commit('SET_NAME', name)
commit('SET_ROLES', roles)
commit('SET_AVATAR', avatar)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
// user logout
logout({
commit,
state
}) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
removeToken() // must remove token first
resetRouter()
commit('RESET_STATE')
commit('SET_ROLES', [])
resolve()
}).catch(error => {
reject(error)
})
})
},
// remove token
resetToken({
commit
}) {
return new Promise(resolve => {
removeToken() // must remove token first
commit('RESET_STATE')
resolve()
})
}
}
export default {
// 加上这个会有报错,不加的话user/login这种方式用不了
namespaced: true,
state,
mutations,
actions
}
4. store/modules/permission.js筛选路由
store/modules/permission.js
用于匹配权限,筛选角色对应的路由并存储起来
import {
asyncRoutes,
constantRoutes
} from '@/router'
/**
* 使用 meta.role 以确定当前用户是否具有权限
* @param roles
* @param route
*/
// 匹配权限
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}
/**
* 通过递归过滤异步路由表
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = {
...route
}
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes) // 将过滤后的路由和constantRoutes存起来
}
}
// 筛选
const actions = {
generateRoutes({
commit
}, roles) {
return new Promise(resolve => {
let accessedRoutes
// 管理员admin显示全部路由,
// 我这里admin是想让它不显示全部的 想要admin能看见全部的话把注释去掉
// if (roles.includes('admin')) {
// accessedRoutes = asyncRoutes || []
// } else {
//过滤路由
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
// accessedRoutes这个就是当前角色可见的动态路由
// }
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
这里的代码说白了就是干了一件事,通过用户的权限和之前在router.js里面asyncRoutes的每一个页面所需要的权限做匹配,最后返回一个该用户能够访问路由有哪些。
5. store/getter.js和store/index.js
当然了,新加的模块要记得引入进去
store/getter.js
添加以下代码:
store/index.js
6. sidebar使用筛选后的路由
src\layout\components\Sidebar\index.vue
遍历之前算出来的permission_routers
,通过vuex拿到之后动态v-for渲染
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
效果
如何看效果?如果是用的vue-admin-template的mock做登录和获取用户数据,登录用户名为admin则role为admin,用户名为editor则role为editor。
角色为admin看到的菜单栏
角色为editor看到的菜单栏
完整源码
记得给我一个star
https://gitee.com/yyy1203/vue-admin-template-permission.git
有兴趣可以看看花裤衩大佬文章:https://juejin.cn/post/6844903478880370701#heading-5