转载请注明出处 https://www.cnblogs.com/leesen/p/14640405.html,或将追究侵权责任
一、背景
背景部分引自 vue element admin 花裤衩
先说一说我权限控制的主体思路,前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过
router.addRoutes
动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是绝对安全的,后端的权限验证是逃不掉的。我司现在就是前端来控制页面级的权限,不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每一个后台的请求不管是 get 还是 post 都会让前端在请求
header
里面携带用户的 token,后端会根据该 token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状态码,做出相对应的操作。
作者:花裤衩
链接:https://juejin.cn/post/6844903478880370701
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
CREATE TABLE `cmf_ls_menu` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `path` varchar(255) DEFAULT '#', `pid` int(11) DEFAULT '0', `url` varchar(255) DEFAULT '#', `redirect` varchar(255) DEFAULT '#', `hidden` varchar(10) DEFAULT NULL, `alwaysShow` varchar(10) DEFAULT NULL, `meta` text, `component` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
*菜单表的各个字段对应的是前端route/index.js对应的格式,字段意义同前端,其中pid表示菜单上级的id,顶级菜单为0
用户表
CREATE TABLE `cmf_ls_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `roles` varchar(255) DEFAULT '[]', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
*其中 roles 是 cmf_ls_user_role 表的id的集合,这里是用一个字段简单实现,复杂业务可能需要有一张用户和身份的对照表
身份表
CREATE TABLE `cmf_ls_user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `description` varchar(255) DEFAULT NULL, `menu_ids` varchar(255) DEFAULT NULL, `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
*menu_ids 是 cmf_ls_menu 表的id的集合
其余php控制器、模型等就不在贴出来了,以下把主要的前端获取动态路由的返回的数据格式贴出来:
{ "code": 200, "msg": "", "data": [{ "id": 1, "name": "users", "path": "/users", "pid": 0, "url": "#", "redirect": "/users/list", "hidden": false, "alwaysShow": false, "meta": { "icon": "el-icon-s-help", "status": true, "title": "RBAC权限管理" }, "component": "#", "label": "RBAC权限管理", "children": [{ "id": 2, "name": "userlist", "path": "userlist", "pid": 1, "url": "#", "redirect": "#", "hidden": false, "alwaysShow": false, "meta": { "icon": "el-icon-s-help", "status": true, "title": "用户列表" }, "component": "/rbac/userlist", "label": "用户列表", "children": [] }, { "id": 3, "name": "menulist", "path": "menulist", "pid": 1, "url": "#", "redirect": "#", "hidden": false, "alwaysShow": false, "meta": { "icon": "el-icon-s-help", "status": true, "title": "菜单管理" }, "component": "/rbac/menulist", "label": "菜单管理", "children": [] }, { "id": 10, "name": "RoleList", "path": "rolelist", "pid": 1, "url": "#", "redirect": "#", "hidden": false, "alwaysShow": false, "meta": { "icon": "", "status": true, "title": "身份管理" }, "component": "/rbac/rolelist", "label": "身份管理", "children": [] }] }, { "id": 4, "name": "icon", "path": "/icon", "pid": 0, "url": "#", "redirect": "#", "hidden": false, "alwaysShow": false, "meta": { "icon": "icon", "status": true, "title": "icon" }, "component": "#", "label": "icon", "children": [{ "id": 9, "name": "Icons", "path": "index", "pid": 4, "url": "#", "redirect": "#", "hidden": false, "alwaysShow": false, "meta": { "icon": "icon", "status": true, "title": "Icons" }, "component": "/icons/index", "label": "Icons", "children": [] }] }, { "id": 5, "name": "Permission", "path": "/permission", "pid": 0, "url": "#", "redirect": "/permission/page", "hidden": false, "alwaysShow": false, "meta": { "icon": "lock", "status": true, "title": "Permission" }, "component": "#", "label": "Permission", "children": [{ "id": 6, "name": "PagePermission", "path": "page", "pid": 5, "url": "#", "redirect": "#", "hidden": false, "alwaysShow": false, "meta": { "icon": "", "status": true, "title": "Page Permission" }, "component": "/permission/page", "label": "Page Permission", "children": [] }, { "id": 7, "name": "DirectivePermission", "path": "directive", "pid": 5, "url": "#", "redirect": "#", "hidden": false, "alwaysShow": false, "meta": { "icon": "", "status": true, "title": "Directive Permission" }, "component": "/permission/directive", "label": "Directive Permission", "children": [] }] }] }
php部分实现了无限级菜单,用户多选身份,身份下配置菜单权限,从而达到不同身份不同菜单的结果。
三、前端部分
首先前端需要把/src/router/index.js 中的asyncRouters即动态路由部分清空
export const asyncRoutes = [];
然后在/src/api/user.js中添加如下方法,定义一个获取后端动态菜单的接口,接口路径和参数换成你自己的
export function getAuthMenu(data){ return request({ url: '/ls/menu/getAuthMenuByRoles', method: 'post', data }) }
下面是/src/store/modules/permission.js的完整代码
import { asyncRoutes, constantRoutes } from '@/router' import { getAuthMenu } from '@/api/user' import Layout from '@/layout' /** * Use meta.role to determine if the current user has permission * @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 } } export const loadView = view => { // 路由懒加载 return resolve => require(['@/views' + view], resolve) } /** * 后台查询的菜单数据拼装成路由格式的数据 * @param routes */ export function generaMenu(routes, data) { data.forEach(item => { const menu = { path: item.path, component: item.component === '#' ? Layout : loadView(item.component), hidden: item.hidden, children: [], name : item.name, meta: item.meta } if(item.pid == 0){ menu.redirect = item.redirect; } if (item.children) { generaMenu(menu.children, item.children) } routes.push(menu) }) routes.push({ path: '*', redirect: '/404', hidden: true }) } /** * Filter asynchronous routing tables by recursion * @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) } } const actions = { generateRoutes({ commit }, roles) { return new Promise(resolve => { const loadMenuData = [] getAuthMenu(state.token).then(response => { let data = response if (response.code !== 200) { alert(JSON.stringify('菜单数据加载异常')) } else { data = response.data Object.assign(loadMenuData, data) const tempAsyncRoutes = Object.assign([], asyncRoutes) generaMenu(tempAsyncRoutes, loadMenuData) let accessedRoutes if (roles.includes('admin')) { accessedRoutes = tempAsyncRoutes || [] } else { accessedRoutes = filterAsyncRoutes(tempAsyncRoutes, roles) } commit('SET_ROUTES', accessedRoutes) resolve(accessedRoutes) } }).catch(error => { console.log(error) }) }) } } export default { namespaced: true, state, mutations, actions }
里面的 generateRoutes 方法 和 generaMenu 方法是用来获取后端返回的菜单,然后处理成前端需要的router的格式
最后是后台菜单界面截图和说明
用户管理
身份设置
身份管理
身份权限设置
这里使用了 el-tree ,选中菜单后给后台传递的就是,tree.getCheckedKeys(),也就是包含各个选中菜单的id的集合(子级全选那么父级也会选中,子级没有全选那么父级会是半选,此时获取的选中key里就不会有父级,我们就将这个数据原样传递给后台。后台返回的动态菜单里,需要递归往上根据菜单PID判断查找父级的id,最终实现即时子菜单不是全选的,返回的菜单里也有父级,当然子菜单一个都没选的话,父级肯定也不需要了,否则前端的菜单是不会展示没父级的选中菜单的)
然后是菜单的管理
pid因为是无限级,所以后台需要返回如下树结构
其中 顶级菜单 是前端固定拼到后台给的树结构中的,可能会如下图
其中splpre字段就是子菜单的前缀,顶级无前缀,下级有一个 ' - ',再下级就是 ‘ - - ’,以此类推(这里是后端给的,主要为了让树结构看起来清晰)
需要特别说明的是添加菜单这里,如果是顶级菜单无下级,那么肯定需要在component里填上组件的路径,如文件路径 /src/view/user/index.vue ,就需要在component字段写上 /user/index,因为permission.js中loadview方法拼接了模板;如果是父级菜单有下级,那么component填上#就行。
另外需要特别说明的是 redirect字段,如果是顶级菜单有下级,则需要填上默认能跳转到子菜单的path。
如果前端改完后报 Module build failed (from ./node_modules/eslint-loader/index.js): TypeError: Cannot read property 'range' of null,
一般是由下图部分引起的(如果使用() => import(`@/views${item.component}`)导入的话)
本人亲测,通过对babel-eslint 降级到7.2.3成功处理这个问题。。。具体方法百度
至此简易的后台配置vue element admin 动态菜单 动态路由 动态权限 就完成了。