根据用户的角色权限生成动态路由
前言
此次Demo的项目是前后端分离,前端使用的是:ant-design-vue-pro,后端使用的是:若依,选择这两个框架的原因是这两个前后端框架对一些基本功能都做了很好的封装,而且学习成本很低,拿来即用。
要解决的问题
若依这个框架它前端本身即是基于vue的前后端分离框架,对于前端权限控制这块主要交给了组件Emlemt ui ,但是我更喜欢使用ant design 所以我选择了antd vue pro,所以这次要解决的问题就是基于用户权限通过antd-vue-pro提供的方法生成用户可访问的路由。
第一步:选择解决方案
目前antd-vue-pro提供了两种基于用户权限生成可访问路由的方式,这里我们就要选择一种自己喜欢的方式,他们分别是:
-
方案一
前端固定路由表和权限配置,由后端提供用户权限标识,来识别是否拥有该路由权限。 -
方案二
后端提供权限和路由信息结构接口,动态生成权限和菜单
方案一就是在路由文件中生成所有页面的路由,然后用户登陆时调用后端接口,后端提供可访问路由的标识符,然后andt这个框架利用其特定方法将静态路由与返回的标识符一一对比,不相等的就过滤掉,将最终该用户可访问的合法路由挂载。
方案二则是摒弃了第一种生成静态路由的方式,通过后端返回一套完整的用户可访问的路由表,然后通过antd这个框架的方法,将返回的路由表组装,然后生成最终的路由动态挂载。
其实这两种方案都最终的实现方式与目的都是一样的,都是生成最终可访问的路由,然后挂载至路由器。这里我选择的是第二种,会了第二种第一肯定也就会了。
最终实现
我是一个前端新手,主攻后端如有说错的地方,还请见谅。
我就从项目的执行路径来说这个Demo
- permission.js
import router from './router'
import store from './store'
import notification from 'ant-design-vue/es/notification'
import NProgress from 'nprogress' // progress bar
import '@/components/NProgress/nprogress.less' // progress bar custom style
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import { getToken } from '@/utils/cookiesUtil'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['login'] // 跳转白名单
const loginRoutePath = '/system/login'
// const routePath = '/'
const defaultRoutePath = '/system/index'
router.beforeEach((to, from, next) => {
NProgress.start()
to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${domTitle}`))
// 因为我们这个项目是好几个子系统,所以用户的token信息存在来cookie里面 //getToken()也就是这个方法里面
if (getToken() !== undefined) {
if (to.path === '/') {
next({ path: defaultRoutePath })
NProgress.done()
} else {
/**
// 获取用户信息
GetInfo: function ({ commit }) {
return new Promise((resolve, reject) => {
getInfo().then(response => {
const result = response.user
// 设置姓名,设置头像
commit('SET_INFO', result)
commit('SET_ROLES', result.roles)
commit('SET_NAME', { name: result.userName, welcome: welcome() })
commit('SET_AVATAR', result.avatar)
setName(result.userName)
resolve(response)
}).catch(error => {
reject(error)
})
})
},*/
// 这个判断是防止用户疯狂刷新页面 ,所以判断一下store有没有用户信息,
//如果没有用户信息则调用 src/user.js中的GetInfo方法,在上方注解中
if (store.getters.roles.length === 0) {
// 设置登陆用户信息 比如头像姓名等
store.dispatch('GetInfo').then(res => {
// 动态生成当前用户等路由表
store.dispatch('GenerateRoutes').then(res => {
// 将此路由挂载
router.addRoutes(store.getters.addRouters)
// 请求带有 redirect 重定向时,登录自动重定向到该地址
const redirect = decodeURIComponent(from.query.redirect || to.path)
if (to.path === redirect) {
next({ ...to, replace: true })
} else {
// 跳转目的路由
next({ path: defaultRoutePath })
}
})
}).catch(() => {
notification.error({
message: '错误',
description: '请求用户信息失败,请重试'
})
// 失败时,获取用户信息失败时,调用登出,来清空历史保留信息
store.dispatch('Logout').then(() => {
next({ path: loginRoutePath, query: { redirect: to.fullPath } })
})
})
} else {
next()
}
}
} else {
if (whiteList.includes(to.name)) {
next()
} else {
next({ path: loginRoutePath, query: { redirect: to.fullPath } })
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
上面这个文件第一步判断用户是否登陆,如果登陆则进入,如果此前没有用户信息则调用getinfo 中后调用GenerateRoutes方法,这里大家需要注意一下:
permission.js中的GenerateRoutes的实现方法有两个来源,大家一定要选择对,andt这个框架默认选择的是方案一,所以我们要在src/store/index.js中将方案二的执行引入取消注释。
// dynamic router permission control (Experimental) 方案二
import permission from './modules/async-router'
我们来看看GenerateRoutes在modules/async-router中的实现,这个方法就是实现生成基于用户权限动态生成路由的关键。
GenerateRoutes代码分析
modules/async-router.js
/**
* 通过index选择,动态生成路由
*/
import { constantRouterMap } from '@/config/router.config'
import { generatorDynamicRouter } from '@/router/generator-routers'
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
}
},
actions: {
// 这里是permission.js中的GenerateRoutes的最终实现
GenerateRoutes ({ commit }) {
return new Promise(resolve => {
//这里调用了/router/generator-routers的方法,我们点进去看看这个方法的实现
generatorDynamicRouter().then(routers => {
commit('SET_ROUTERS', routers)
resolve()
})
})
}
}
}
export default permission
下面这个方法至关重要!是根据后端返回数据动态路由的核心组装方法
/router/generator-routers.js
import * as loginService from '@/api/login'
import { UserLayout, BlankLayout, CommonLayout } from '@/layouts'
// 保存状态组件
const RouteView = {
name: 'RouteView',
render: (h) => h('router-view')
}
// 前端路由表
const constantRouterComponents = {
// 基础页面 layout 必须引入
BlankLayout: BlankLayout,
// 保存页面状体的组件
RouteView: RouteView,
UserLayout: UserLayout,
// 这个是我权限页面同样的头部组件(自定义的)
CommonLayout: CommonLayout,
//这个是将组件与component关联,假如我现在在有三个页面,分别是用户,角色,部门,那我组装数据的时候component user必须对应user
// 你们看看数据结构跟下面generator()方法就知道了。
'User': () => import('@/views/security/UserMent'),
'Role': () => import('@/views/security/RoleMent'),
'Dept': () => import('@/views/security/DeptMent')
}
// 前端未找到页面路由(固定不用改)
const notFoundRouter = {
path: '*', redirect: '/404', hidden: true
}
/**
* 动态生成菜单
* @param token
* @returns {Promise<Router>}
*/
// 这里就是modules/async-router/generatorDynamicRouter的具体实现
export const generatorDynamicRouter = () => {
return new Promise((resolve, reject) => {
// 调用后端接口,返回路由表
loginService.getCurrentUserNav().then(res => {
const { data } = res
// 组装数据
const routers = generator(data)
// 将404路由追加在刚刚组装好的路由中
routers.push(notFoundRouter)
// 返回最终路由
resolve(routers)
}).catch(err => {
reject(err)
})
})
}
/**
* 格式化树形结构数据 生成 vue-router 层级路由表
*
* @param routerMap
* @param parent
* @returns {*}
*/
export const generator = (routerMap, parent) => {
return routerMap.map(item => {
const { title, icon } = item.meta || {}
const currentRouter = {
// 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace
path: item.path || `${parent && parent.path || ''}/${item.key}`,
// 路由名称,建议唯一
name: item.name || item.key || '',
// 该路由对应页面的组件,这个跟下面的没什么很大的区别,自己体会
// component: constantRouterComponents[item.component || item.key],
// 该路由对应页面的组件 (动态加载)
// 假如我这里面的item.component是:CommonLayout 则此路由的component就是上方constantRouterComponents中的CommonLayout组件
//假如我这里的item.key是User则component则为
//constantRouterComponents中的 'User': () => import('@/views/security/UserMent'),
component: (constantRouterComponents[item.component || item.key]) || (() => import(`@/views/${item.component}`)),
// meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
meta: {
title: title,
icon: icon || undefined,
permission: item.name
}
}
// 是否设置了隐藏子菜单
if (item.hideChildrenInMenu) {
currentRouter.hideChildrenInMenu = true
}
// 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
if (!currentRouter.path.startsWith('http')) {
currentRouter.path = currentRouter.path.replace('//', '/')
}
// 重定向
item.redirect && (currentRouter.redirect = item.redirect)
// 是否有子菜单,并递归处理
if (item.children && item.children.length > 0) {
// Recursion
currentRouter.children = generator(item.children, currentRouter)
}
// 返回组装好的数据
return currentRouter
})
}
后端返回的数据结构json
{"msg":"操作成功","code":200,"data":[{"title":"首页","name":"securtiy","path":"/securtiy","hidden":false,"hideChildrenInMenu":false,"redirect":"/user","component":"CommonLayout","children":[{"title":"用户管理","key":"User","path":"/user","hidden":false,"hideChildrenInMenu":true,"redirect":"/security/UserMen","component":"RouteView","meta":{"title":"用户管理","icon":"user"},"children":[{"title":"用户管理","key":"User","path":"/security/UserMen","hidden":false,"hideChildrenInMenu":false,"component":"User","meta":{"title":"用户管理","icon":"user"}}]},{"title":"角色管理","key":"Role","path":"/role","hidden":false,"hideChildrenInMenu":true,"redirect":"/security/RoleMen","component":"RouteView","meta":{"title":"角色管理","icon":"idcard"},"children":[{"title":"角色管理","key":"Role","path":"/security/RoleMen","hidden":false,"hideChildrenInMenu":false,"component":"Role","meta":{"title":"角色管理","icon":"idcard"}}]},{"title":"部门管理","key":"Dept","path":"/dept","hidden":false,"hideChildrenInMenu":true,"redirect":"/security/DeptMen","component":"RouteView","meta":{"title":"部门管理","icon":"cluster"},"children":[{"title":"部门管理","key":"Dept","path":"/security/DeptMen","hidden":false,"hideChildrenInMenu":false,"component":"Dept","meta":{"title":"部门管理","icon":"cluster"}}]}]}]}
到这里我们的路由就生成,当/router/generator-routers.js中的generatorDynamicRouter将路由生成并返回,此时modules/async-router.js中的generatorDynamicRouter方法会将生成的路由与我们的基础路由合并,就是下面的这段代码。
/**
* 通过index选择,动态生成路由
*/
import { constantRouterMap } from '@/config/router.config'
import { generatorDynamicRouter } from '@/router/generator-routers'
const permission = {
state: {
///config/router.config 中的基础路由
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
//将组装好的路由加入state中
state.addRouters = routers
// 将组装好的路由信息与基础路由合并
state.routers = constantRouterMap.concat(routers)
}
},
actions: {
// 这里是permission.js中的GenerateRoutes的最终实现
GenerateRoutes ({ commit }) {
return new Promise(resolve => {
//这里调用了/router/generator-routers的方法,我们点进去看看这个方法的实现
generatorDynamicRouter().then(routers => {
//将返回组装好的路由信息追加与我们的基础路由合并。
commit('SET_ROUTERS', routers)
resolve()
})
})
}
}
}
export default permission
我的基础路由是这样的
然后最后一步就是加我们的路由动态挂载,也就是博客开头的permission.js中的这段代码
if (store.getters.roles.length === 0) {
// 设置登陆用户信息 比如头像姓名等
store.dispatch('GetInfo').then(res => {
// 动态生成当前用户等路由表
store.dispatch('GenerateRoutes').then(res => {
// 将此路由挂载 !!!!!!!!!!!!!!!!
router.addRoutes(store.getters.addRouters)
// 请求带有 redirect 重定向时,登录自动重定向到该地址
const redirect = decodeURIComponent(from.query.redirect || to.path)
if (to.path === redirect) {
next({ ...to, replace: true })
} else {
// 跳转目的路由
next({ path: defaultRoutePath })
}
})
}).catch(() => {
notification.error({
message: '错误',
description: '请求用户信息失败,请重试'
})
// 失败时,获取用户信息失败时,调用登出,来清空历史保留信息
store.dispatch('Logout').then(() => {
next({ path: loginRoutePath, query: { redirect: to.fullPath } })
})
})
} else {
next()
}
- 到这我们的工作基本就完成了,但是这仅仅只是精确到页面的用户权限,我们应该定位到按钮才对,基于按钮的用户权限其实antd这个框架也提供了很好的支持,我的下篇博客将会详细的说明。
- 此外对于基于用户权限生成路由的方案选择,方案一与方案二前期的流程可能会有差异,但是最后实现都是一样的,都是使用 router.addRoutes()将路由挂载。
好了,今天的博客就到这了,为什么我会写这篇博客呢,第一这是我第一次使用vue+ant做用户的权限管理系统,其实在做的时候遇到不少问题,在此记录,以提醒自己,另外如果能帮到大家,那将会是我的荣幸,原创不易,切勿搬运。