权限管理在后台应用中特别常见,通过本文你将了解到权限管理中的几个难点的解决方案:权限的加载与卸载, 基于权限的动态菜单生成、基于路由钩子的动态路由,权限本地持久化与状态管理,首页跳转机制,404机制等
权限定义
定义好菜单、路由表(页面)、按钮权限集,后续可通过权限标志匹配权限集。
// 菜单权限
const PERMISSION_MENU = {
merchantMgt: 'merchantMgt', // 客户管理
device: 'device', // 设备管理
orderMgt: 'orderMgt', // 订单管理
financeMgt: 'financeMgt', // 财务管理
};
// 路由权限
export const PERMISSION_ROUTE = {
merchantInParts: 'merchantInParts', // 商户进件
merchantIndex: 'merchantIndex', // 商户列表
device: 'device', // 设备管理
orderOverview: 'orderOverview', // 交易概览
orderDetail: 'orderDetail', // 交易订单
orderRefund: 'orderRefund', // 退款订单
financeOverview: 'financeOverview', // 对账概览
};
// 按钮权限
export const PERMISSIONS = {
[PERMISSION_ROUTE.merchantInParts]: {
export: 'merchantInParts:export',
resend: 'merchantInParts:resend',
close: 'merchantInParts:close',
},
[PERMISSION_ROUTE.merchantIndex]: {
add: 'merchantIndex:add',
modify: 'merchantIndex:modify',
delete: 'merchantIndex:delete',
},
[PERMISSION_ROUTE.device]: {
add: 'device:add',
modify: 'device:modify',
delete: 'device:delete',
relation: 'device:relation',
unbinding: 'device:unbinding',
},
[PERMISSION_ROUTE.orderDetail]: {
export: 'orderDetail:export',
},
[PERMISSION_ROUTE.orderRefund]: {
export: 'orderRefund:export',
},
};
// 权限路由
export const permissionRoutes = [
{
path: '/merchantInParts',
component: () => import(/* webpackChunkName: "merchantInParts" */ '../views/merchantMgt/inParts.vue'),
meta: { title: '商户进件', permission: PERMISSION_ROUTE.merchantInParts },
},
{
path: '/merchantIndex',
component: () => import(/* webpackChunkName: "merchantIndex" */ '../views/merchantMgt/index.vue'),
meta: { title: '商户列表', permission: PERMISSION_ROUTE.merchantIndex },
},
{
path: '/device',
component: () => import(/* webpackChunkName: "device" */ '../views/device/index.vue'),
meta: { title: '设备管理', permission: PERMISSION_ROUTE.device },
},
{
path: '/orderOverview',
component: () => import(/* webpackChunkName: "orderOverview" */ '../views/orderMgt/overview/index.vue'),
meta: { title: '交易概览', permission: PERMISSION_ROUTE.orderOverview },
},
{
path: '/orderDetail',
component: () => import(/* webpackChunkName: "orderDetail" */ '../views/orderMgt/detail/index.vue'),
meta: { title: '交易订单', permission: PERMISSION_ROUTE.orderDetail },
},
{
path: '/orderRefund',
component: () => import(/* webpackChunkName: "orderRefund" */ '../views/orderMgt/refund/index.vue'),
meta: { title: '退款订单', permission: PERMISSION_ROUTE.orderRefund },
},
{
path: '/financeOverview',
component: () => import(/* webpackChunkName: "financeOverview" */ '../views/financeMgt/overview/index.vue'),
meta: { title: '对账概览', permission: PERMISSION_ROUTE.financeOverview },
},
];
// 权限菜单
export const permissionMenus = [
{
icon: 'el-icon-user-solid',
index: PERMISSION_MENU.merchantMgt,
title: '商户管理',
subs: [
{
code: PERMISSION_ROUTE.merchantInParts,
index: getPermissionRoutePath(PERMISSION_ROUTE.merchantInParts),
title: '商户进件',
},
{ code: PERMISSION_ROUTE.merchantIndex, index: getPermissionRoutePath(PERMISSION_ROUTE.merchantIndex), title: '商户列表' },
],
},
{
icon: 'el-icon-copy-document',
code: PERMISSION_MENU.device,
index: getPermissionRoutePath(PERMISSION_ROUTE.device),
title: '设备管理',
},
{
icon: 'el-icon-s-order',
index: PERMISSION_MENU.orderMgt,
title: '订单管理',
subs: [
{
code: PERMISSION_ROUTE.orderOverview,
index: getPermissionRoutePath(PERMISSION_ROUTE.orderOverview),
title: '交易概览',
},
{
code: PERMISSION_ROUTE.orderDetail,
index: getPermissionRoutePath(PERMISSION_ROUTE.orderDetail),
title: '交易订单',
},
{
code: PERMISSION_ROUTE.orderRefund,
index: getPermissionRoutePath(PERMISSION_ROUTE.orderRefund),
title: '退款订单',
},
],
},
{
icon: 'el-icon-s-finance',
index: PERMISSION_MENU.financeMgt,
title: '财务管理',
subs: [
{
code: PERMISSION_ROUTE.financeOverview,
index: getPermissionRoutePath(PERMISSION_ROUTE.financeOverview),
title: '对账概览',
},
],
},
];
function getPermissionRoutePath(permission) {
return permissionRoutes.find((route) => route.meta.permission === permission)?.path;
}
权限树匹配
权限匹配后的权限树形结构大致如下:
对应方法:
// 根据权限匹配授权路由表
export function getPermissionRoutes(permissionsTree) {
const permissionsMap = {};
function reCallback(tree = []) {
tree.forEach((v) => {
permissionsMap[v.code] = true;
if (v.children) {
reCallback(v.children);
}
});
}
reCallback(permissionsTree);
return permissionRoutes.filter((route) => !route.meta.permission || permissionsMap[route.meta.permission]);
}
// 根据权限匹配菜单
export function getPermissionMenus(permissionsTree, menus = permissionMenus) {
if (!permissionsTree) {
return [];
}
const ans = [];
menus.forEach((menu) => {
// 菜单取index, page取code
const tree = permissionsTree.find((v) => v.code === (menu.code ?? menu.index));
if (tree) {
if (menu.subs) {
const subs = tree.children ? getPermissionMenus(tree.children, menu.subs) : [];
if (subs.length) {
ans.push({
...menu,
subs,
});
}
} else {
ans.push(menu);
}
}
});
return ans;
}
const cache = new Map();
// 判断是否含有该授权标志
export function hasPermission(permission) {
if (!permission) {
return true;
}
// 获取所有的权限
const permissionMap = cache.get(store.getters.permissionTree);
if (permissionMap) {
return permissionMap.has(permission);
} else {
let map = new Map();
let list = store.getters.permissionTree;
while (list.length) {
let newArr = [];
list.forEach((v) => {
map.set(v.code, true);
if (v.children) {
newArr = newArr.concat(v.children);
}
});
list = newArr;
}
cache.clear();
cache.set(store.getters.permissionTree, map);
return map.has(permission);
}
}
路由的加载与重载
先定义好静态路由表,404在动态路由中最后添加,以便没有授权的用户与授权的用户都能成功响应404页面,通过ADD_ROUTES设置是否加载路由,可实现登录,登出手动加载与卸载权限路由,redirect函数中根据路由权限自定重定向到首页。
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from '@/store';
import { ADD_ROUTES } from '@/store/mutation-types';
Vue.use(VueRouter);
// 静态路由表
const constantRouters = [
{
path: '/login',
component: () => import(/* webpackChunkName: "login" */ '../components/page/Login.vue'),
meta: { title: '登录' },
},
{
path: '/404',
component: () => import(/* webpackChunkName: "404" */ '../components/page/404.vue'),
meta: { title: '404' },
},
{
path: '/403',
component: () => import(/* webpackChunkName: "403" */ '../components/page/403.vue'),
meta: { title: '403' },
},
{
path: '/test',
component: () => import('../components/page/test.vue'),
meta: { title: 'test' },
},
];
const router = new VueRouter({
mode: 'history',
base: import.meta.env.BASE_URL,
routes: constantRouters,
});
// 路由加载标识设置
export function setAddRoutes(val) {
store.commit(ADD_ROUTES, val);
}
//使用钩子函数对路由进行权限跳转
router.beforeEach((to, from, next) => {
document.title = to.meta?.title ? `${to.meta.title} | 管理后台` : '管理后台';
const token = localStorage.getItem('token');
if (!token && to.path !== '/login') {
next({
path: '/login',
replace: true,
});
} else {
// 手动添加route 考虑登出登录之后,isAddRoute仍然存在
if (!store.state.addRoutes) {
// 根据name覆盖原有的路由
router.addRoute({
name: 'Index',
path: '/',
redirect: () => {
const routes = store.getters.routes;
return routes[0]?.path ?? '/login';
},
component: () => import(/* webpackChunkName: "home" */ '../components/common/Home.vue'),
meta: { title: '首页' },
children: store.getters.routes,
});
// 覆盖默认的 404
router.addRoute({
// 会匹配所有路径
name: '404',
path: '*',
redirect: '/404',
});
setAddRoutes(true);
// 未加载动态路由之前(如:刷新页面)初始化的时候会被重定向到404页面,所以这里需要回退到 redirectedFrom
const toPath = to.path === '/404' && to.redirectedFrom ? to.redirectedFrom : to.fullPath;
//触发重定向 router.vuejs.org/zh/guide/advanced/dynamic-routing.html#%E5%9C%A8%E5%AF%BC%E8%88%AA%E5%AE%88%E5%8D%AB%E4%B8%AD%E6%B7%BB%E5%8A%A0%E8%B7%AF%E7%94%B1
next(toPath);
} else {
next();
}
}
});
export default router;
状态持久化处理
import router from '@/router';
import { setLocal } from '@/utils/storage';
import { ADD_ROUTES } from '../mutation-types';
const user = {
actions: {
async login({ commit, dispatch }, data) {
setLocal('loginInfo', data);
localStorage.setItem('token', data.access_token);
localStorage.setItem('ms_username', data.loginId);
if (!data.staffman?.length) {
commit('SET_PERMISSION', []);
commit(ADD_ROUTES, false);
return;
}
await dispatch('getPermissionData', data.staffman?.[0]).then(() => {
commit(ADD_ROUTES, false);
});
},
logout({ commit }) {
localStorage.removeItem('loginInfo');
localStorage.removeItem('ms_username');
localStorage.removeItem('token');
commit('SET_PERMISSION', []);
commit(ADD_ROUTES, false);
router.push('/login');
},
},
};
export default user;
import { getTreePrivileges } from '@/api/account';
import { getLocal, setLocal } from '@/utils/storage';
import { ADD_ROUTES } from '../mutation-types';
const permission = {
state: {
permissionTree: getLocal('permissionTree') || [],
},
mutations: {
SET_PERMISSION(state, permissions) {
state.permissionTree = permissions;
setLocal('permissionTree', permissions);
},
},
actions: {
clearPermissions({ commit }) {
commit('SET_PERMISSION', []);
commit(ADD_ROUTES, false);
},
getPermissionData({ commit }, { market, marketName, marketId } = {}) {
return getTreePrivileges({
market,
marketName,
marketId,
terminal: 'Web聚合',
}).then((res) => {
commit('SET_PERMISSION', res.data.data);
});
},
},
};
export default permission;
import { getPermissionMenus, getPermissionRoutes } from '@/utils/permission';
const getters = {
token: (state) => state.user.token,
userinfo: (state) => state.user.userInfo,
permissionTree: (state) => state.permission.permissionTree,
menus: (state) => getPermissionMenus(state.permission.permissionTree),
routes: (state) => getPermissionRoutes(state.permission.permissionTree),
};
export default getters;
login(data).then((res) => {
this.$message.success('登录成功');
this.$store.dispatch('login', res.data.data).then(() => {
// web ,没有配置权限的用户登录管理后台时,提示优化
if (!this.$store.getters.permissionTree?.length) {
this.$message.error('登录用户,暂无管理权限');
return;
}
this.$router.push({
name: 'Index',
});
});
});
全局方法和指令
指令没有全局方法好用,设置好页面与按钮之前的关联,可以直接读取route.meta.permission搭配按钮标识即可直接使用
全局方法:
Vue.prototype.hasPermission = function (val) {
const metaPermission = this.$route.meta.permission;
if (!metaPermission) {
return false;
}
return hasPermission(PERMISSIONS[metaPermission]?.[val]);
};
<template>
<el-button v-if="hasPermission('add')" type="primary" icon="el-icon-plus" @click="addCustomer">新增</el-button>
</template>
指令
import { hasPermission } from '@/utils/permission';
export default {
inserted(el, binding, vnode) {
const { value } = binding;
if (typeof value !== 'string') {
throw new Error('need roles Like v-permission=[admin]');
}
if (!hasPermission(value)) {
el.parentNode && el.parentNode.removeChild(el);
}
},
};
import permission from './permission';
import Vue from 'vue';
Vue.directive('permission', permission);
<template>
<el-button v-permission="'device:add'" type="primary" icon="el-icon-plus" @click="addCustomer">新增</el-button>
</template>