权限访问控制模型 采用RBAC,即Role-based access control,基于角色的访问控制。
每个用户关联一个或多个角色,每个角色关联一个或多个权限,从而可以实现了非常灵活的权限管理。如下图。
更多的权限访问控制模型参考这里 。
权限设计:初步方案 采用Ant Design Pro中的默认方案 , 即前端固定路由表和权限配置,通过后端接口获取到用户拥有的权限代码,来识别是否拥有路由权限或操作权限。
大概的流程如下:
实现:权限配置 需要考虑的点:
权限配置需要支持国际化
大多数模块的权限都有增删改查
最终实现:
用文件目录结构体现权限层次
层次固定为3级
第一级为大分类,例如系统设置、功能配置等
第二级为模块名称,例如用户管理、角色管理等
第三级为具体的 Action,例如新增、修改、删除、读取等
配置
固定在permissions目录下编写
1 2 3 4 |--permissions/ | |--admin/ | | |--user.json | | |--role.json
权限的第一级是permissions下的子目录(例如admin)。
权限的第二级是配置文件的文件名。
权限的第三级在JSON中书写,例如["read", "create", "edit", "delete"]
,由于通常增删查改是模块的基础权限,支持"CRUD"
表示增删查改来简化配置, 也支持用CRUD的子集来表示部分权限,比如"R"
表示仅有读取的权限。
翻译
在locales/permissions.i18n.js
中进行翻译配置。
CRUD四种基础权限不用翻译。
权限配置上传工具 我们需要写一个权限配置上传工具来将上传到后端数据库。
步骤:
获取所有权限code
写入数据库
我用python写了一个,大概长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def _get_all_permissions_code () : files = [y for x in os.walk(PERMISSION_CONF_DIR) for y in glob.glob(os.path.join(x[0 ], '*.json' ))] total = [] for file in files: match = re.search(re.compile(PERMISSION_CONF_DIR + "/(.*?).json" ), file) if match: prefix = match.group(1 ) items = [] with open(file, 'r' ) as f: for item in json.load(f): crud_match = re.search(r'^[CRUD]+$' , item) if crud_match: for crud_item in crud_match.group(0 ): if crud_item == "C" : items.append("create" ) elif crud_item == "R" : items.append("read" ) elif crud_item == "U" : items.append("update" ) elif crud_item == "D" : items.append("delete" ) total = total + ['.' .join(f"{prefix} /{x} " .split('/' )) for x in items] return total
上传后我们数据库的权限表大概长这样:
实现:路由表配置 除了动态生成的路由,我们有一些基础的不需要动态的路由,例如登录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export const basicRouters = [ { path: "/user" , component: UserLayout, redirect: "/user/login" , children: [ { path: "login" , name: "login" , component: () => import ("@/views/login/Login" ) } ] }, { path: "/404" , component: () => import ("@/views/exception/404" ) } ];
在动态路由的配置中,我们可以在meta中进行权限配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 export const dynamicRouters = [ { path: "/" , name: "index" , component: BasicLayout, meta: { title : "menu.home" }, redirect: "/dashboard" , children: [ { path: "/dashboard" , name: "dashboard" , component: () => import ("@/views/dash/Dashboard" ), meta: { title : "menu.dashboard" , icon : "dashboard" } }, { path: "/admin" , component: RouteView, meta: { title : "menu.admin.default" , icon : "setting" }, children: [ { path: "/admin/user" , name: "user" , component: () => import ("@/views/admin/user/User" ), meta: { title : "menu.admin.user" , permission : ["admin.user.read" ] } }, { path: "/admin/role" , name: "role" , component: () => import ("@/views/admin/role/Role" ), meta: { title : "menu.admin.role" , permission : ["admin.role.read" ] } } ] } ] }, { path: "*" , redirect: "/404" } ];
实现:路由钩子 我们将动态生成路由的逻辑放在路由钩子中。
大概长这样:
router/router-hook.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 const allowList = ["login" ];const loginRoutePath = "/user/login" ;const defaultRoutePath = "/dashboard" ;router.beforeEach((to, from , next ) => { if (store.getters.token) { if (to.path === loginRoutePath) { next({ path : defaultRoutePath }); } else { if (store.getters.additionalRouters.length === 0 ) { store .dispatch("user/getUserInfo" ) .then(res => { const permissions = res.permissions; store.dispatch("permission/generateRouters" , { permissions }).then(() => { store.getters.additionalRouters.forEach(item => { router.addRoute(item); }); const redirect = decodeURIComponent (from .query.redirect || to.path); if (to.path === redirect) { next({ ...to, replace : true }); } else { next({ path : redirect }); } }); }) .catch(() => { store.dispatch("user/logout" ).then(() => { next({ path : loginRoutePath, query : { redirect : to.fullPath } }); }); }); } else { next(); } } } else { if (allowList.includes(to.name)) { next(); } else { next({ path : loginRoutePath, query : { redirect : to.fullPath } }); } } });
实现:Store 在Store中实现动态路由的逻辑并存储动态的路由表
store/modules/permission.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 function hasPermission (router, permissions ) { if (router.meta && router.meta.permission) { return !router.meta.permission.some(val => permissions.indexOf(val) === -1 ); } return true ; } function filterRouters (routers, permissions ) { const accessedRouters = routers.filter(router => { if (hasPermission(router, permissions)) { if (router.children && router.children.length) { router.children = filterRouters(router.children, permissions); } return true ; } return false ; }); return accessedRouters; } const state = { routers: basicRouters, additionalRouters: [] }; const mutations = { SET_ROUTERS: (state, routers ) => { state.additionalRouters = routers; state.routers = basicRouters.concat(routers); } }; const actions = { generateRouters({ commit }, data) { return new Promise (resolve => { const { permissions } = data; const accessedRouters = store.getters.username === "admin" ? dynamicRouters : filterRouters(_cloneDeep(dynamicRouters), permissions); commit("SET_ROUTERS" , accessedRouters); resolve(); }); } };
实现:操作权限控制 我们可以给Vue注册一个全局方法,用来验证是否有权限。
写一个插件。
plugin\auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const plugin = { install: (Vue, { store } ) => { if (!store) { throw new Error ("Please provide vuex store." ); } Vue.prototype.$auth = function (permissions ) { if (store.getters.username === "admin" ) { return true ; } return !permissions.some(val => store.getters.permissions.indexOf(val) === -1 ); }; } }; export default plugin;
这样,我们可以在模版中对组件进行权限控制。
1 2 3 <button v-if="$auth(['code1'])"> Test </button>
为了避免与组件本身的v-if逻辑混在一起写,我们推荐使用指令的方式来实现。
大同小异,我们定义一个auth指令。
directives/auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import Vue from "vue" ;import store from "@/store" ;const auth = Vue.directive("auth" , { inserted: function (el, binding ) { if (store.getters.username === "admin" ) { return ; } let permissions = binding.value; if (typeof permissions === "string" ) { permissions = [permissions]; } if (permissions.some(val => store.getters.permissions.indexOf(val) === -1 )) { if (el.parentNode) { el.parentNode.removeChild(el); } else { el.style.display = "none" ; } } } }); export default auth;
在模版中进行权限控制:
1 2 3 <button v-auth="'admin.user.create'"> Test </button>
实现:动态导航菜单 我们可以使用上面过滤后的动态路由来生成菜单:
1 2 3 4 5 6 7 8 9 computed: { ...mapState({ mainMenu: state => state.permission.additionalRouters }) }, created() { const routes = this .mainMenu.find(item => item.path === "/" ); this .menus = (routes && routes.children) || []; }
回顾一下 回顾一下,主要包括以下几部分。
权限配置:更简化的配置,使用自动化工具上传权限。
动态路由:在路由钩子中实现。
操作权限控制:使用指令。
动态导航菜单:通过动态路由生成。
完整的实现在这里 。
Reference