1.原理
通过后端返回的权限数组,对前端路由进行筛选,筛选出可访问路由,重写路由导航守卫,在路由进入前进行判断,看是否为可访问路由,满足就跳转,不满足就跳转错误页面;在页面通过自定义指令结合返回的权限数组来控制按钮的显示和隐藏。
2.实现
1.获取用户的权限信息
// 获取用户信息,
async GetInfo({ commit }) {
return getInfo().then((response) => {
const userInfo: IUserInfo = response.data;
if (userInfo) {
// 生成自定义权限map
const operationMap = {};
for (const op of userInfo.operations) {
operationMap[op.id] = true;
}
userInfo.c_operation_map = operationMap;
const permissionMap = {};
for (const op of userInfo.moduleIds) {
permissionMap[op] = true;
}
userInfo.c_permission_map = permissionMap;
commit('SET_INFO', userInfo);
if (userInfo.moduleIds.length === 0) {
throw new Error('getInfo: moduleIds must be a non-null array !');
}
} else {
throw new Error('getInfo: no user info !');
}
return response.data;
});
},
2.路由筛选
// store中筛选并存储
export interface IPermissionState {
routers: RouteRecordRaw[];
addRouters: RouteRecordRaw[];
}
function hasPermission(permissions: string[], route: RouteRecordRaw): boolean {
// 特殊地,GENERAL不需要控制权限
if (route.meta?.permission === 'GENERAL') {
return true;
}
let flag = false;
for (let i = 0, len = permissions.length; i < len; i++) {
flag = route.meta?.permission === permissions[i];
if (flag) {
return true;
}
}
return false;
}
// 路由筛选
function filterAsyncRouter(routes: RouteRecordRaw[], moduleIds: string[]) {
const res: RouteRecordRaw[] = [];
for (const route of routes) {
if (hasPermission(moduleIds, route)) {
const routeCopy = cloneDeep(route);
if (!isUndefined(routeCopy.children)) {
routeCopy.children = filterAsyncRouter(route.children!, moduleIds);
if (routeCopy.children!.length > 0) {
routeCopy.redirect = routeCopy.redirect || routeCopy.children[0].path;
}
}
res.push(routeCopy);
}
}
return res;
}
const store: Module<IPermissionState, IAppState> = {
state: {
routers: generalRoutes,
addRouters: [],
},
mutations: {
['SET_ROUTERS'](state, routers) {
state.addRouters = routers;
state.routers = generalRoutes.concat(routers);
},
},
getters: {
addRouters: (state) => {
return state.addRouters;
},
},
actions: {
async GenerateRoutes({ commit }, moduleIds: string[]) {
return new Promise<void>((resolve) => {
getAsyncRoutesByJson().then((res) => {
const asyncRoutes = res.asyncRoutes;
const accessedRouters = filterAsyncRouter(asyncRoutes, moduleIds);
for (const ar of accessedRouters) {
if (!isUndefined(ar.children) && ar.children!.length > 0) {
ar.redirect = ar.redirect || ar.children![0].path;
}
}
commit('SET_ROUTERS', accessedRouters);
resolve();
});
});
},
},
};
3.导航守卫重写
router.beforeEach(async (to, from, next: NavigationGuardNext) => {
to.meta && isNotNil<string>(to.meta.title) && setDocumentTitle(to.meta.title);
store.commit('SET_ROUTE_PATH', to.path);
if (to.meta.permission === 'GENERAL') {
next();
} else {
if (
isNotNil<IUserInfo>((store.state as AllStateTypes).CompositeModule.user)
) {
const accessRoutes = router.getRoutes();
// 如果跳转的路由不在可访问路由表,直接跳转403
const target =
accessRoutes &&
accessRoutes.length &&
accessRoutes.find((item) => {
return item.name === to.name;
});
if (target) {
next();
} else {
next({ path: '/403' });
}
} else {
// 如果用户信息不存在需要重新获取
await store.dispatch('GetUser');
// 获取更新后的用户信息
store
.dispatch(
'GenerateRoutes',
(store.state as AllStateTypes).CompositeModule.user!.modules
)
.then(() => {
// 根据roles权限生成可访问的路由表
// 动态添加可访问路由表(vue-router3和vue-router4中有区别)
store.getters.addRouters.forEach((route: RouteRecordRaw) => {
router.addRoute(route);
});
setRouteRedirect(router.getRoutes());
if (
(
store.state as AllStateTypes
).CompositeModule.user!.modules.indexOf(
to.meta.permission as string
) === -1
) {
if (typeof to.redirectedFrom !== 'undefined') {
const result = setRedirect(
router.getRoutes(),
to.redirectedFrom,
(store.state as AllStateTypes).CompositeModule.user!.modules
);
if (result === '403') {
next({
path: '/403',
});
} else {
if (result.redirect) {
next({
path: result.redirect as string,
});
} else {
if (typeof to.redirectedFrom !== 'undefined') {
next({
path: to.redirectedFrom + '/' + result.path,
});
} else {
next({ path: to.path });
}
}
}
} else {
next({
path: '/403',
});
}
} else {
if (to.path === '/assistant/base/apply') {
// 有用户信息时,阻止进入申请页面
next('/assistant/base/index');
return;
}
next({ ...to, replace: true });
}
})
.catch(() => {
const key = 'notification';
let countDown = 3;
notification.error({
key,
message: '请求用户信息失败',
description: `请重新登录!${countDown}秒后自动跳转登录页面`,
duration: null,
});
const interval = setInterval(() => {
notification.error({
key,
message: '请求用户信息失败',
description: `请重新登录!${countDown - 1}秒后自动跳转登录页面`,
duration: null,
});
countDown = countDown - 1;
}, 1000);
// 获取用户信息失败时,跳转到登录
setTimeout(() => {
clearInterval(interval);
next({ path: '/lgn' });
}, 3000);
});
}
}
});
// 从别处跳转过来的路由判断
export function setRedirect(
routes: RouteRecordRaw[], //筛选后的路由
route: RouteLocation, //重定向来源路由
modules: string[] //模块权限
) {
let sourceRoute = {} as RouteRecordRaw;
let targetRoute = {} as RouteRecordRaw;
routes.forEach((item) => {
if (item.name === route.name) {
sourceRoute = item;
}
});
if (sourceRoute?.children) {
sourceRoute.children.forEach((item) => {
if (modules.indexOf(String(item.meta?.permission)) !== -1) {
targetRoute = item;
}
});
if (typeof targetRoute !== 'undefined') {
return targetRoute;
} else {
return '403';
}
} else {
return '403';
}
}
/**
* 修改全部路由:当路由存在重定向时检查重定向路由是否有权限,没有权限重定向改为第一个子路由,无子路由重定向到403
* @param routes
* @param route
*/
export function setRouteRedirect(routes: RouteRecordRaw[]) {
routes.forEach((pItem) => {
if (pItem.redirect) {
// 查找重定向的路由地址是否有权限
const target = routes.find((item) => {
return item.path === pItem.redirect;
});
if (!target) {
if (pItem.children && pItem.children.length) {
pItem.redirect =
pItem.path +
(/\/$/.test(pItem.path) ? '' : '/') +
pItem.children[0].path;
} else {
pItem.redirect = '/403';
}
}
}
});
}
// 模块权限判断:用于无法使用自定义指令的组件
export function getPermission(name: string) {
if ((store.state as AllStateTypes).CompositeModule.user) {
return (
(store.state as AllStateTypes).CompositeModule.user!.modules.indexOf(
name
) > -1
);
} else {
return false;
}
}
// 操作权限判断:用于无法使用自定义指令的组件
export function getOperation(key: string) {
if ((store.state as AllStateTypes).CompositeModule.user) {
return (
(store.state as AllStateTypes).CompositeModule.user!.operations.indexOf(
key
) > -1
);
} else {
return false;
}
}
4.自定义指令实现
注:vue2和vue3自定义指令钩子函数有所区别
/**
* Action 权限指令
* 指令用法:
* - 在需要控制 operation 级别权限的组件上使用 v-operation:[method] , 如下:
* <i-button v-operation:add >添加用户</a-button>
* <a-button v-operation:delete>删除用户</a-button>
* <a v-operation:edit @click="edit(record)">修改</a>
*
* - 当前用户没有权限时,组件上使用了该指令则会被隐藏
*
*/
export const operation: Directive = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mounted(el: HTMLElement, binding: DirectiveBinding<any>) {
const operationName = binding.arg!;
const userInfo: IUserInfo | null = (store.state as AllStateTypes)
.CompositeModule.user;
if (userInfo && userInfo.c_operation_map[operationName]) {
return;
}
(el.parentNode && el.parentNode.removeChild(el)) ||
(el.style.display = 'none');
},
};
export const permission = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mounted(el: HTMLElement, binding: DirectiveBinding<any>) {
const permissionName = binding.arg!;
const userInfo: IUserInfo | null = (store.state as AllStateTypes)
.CompositeModule.user;
if (userInfo && userInfo.c_permission_map[permissionName]) {
return;
}
(el.parentNode && el.parentNode.removeChild(el)) ||
(el.style.display = 'none');
},
};
5.路由表权限配置
{
path: 'list',
name: 'ctrl.monitor.list',
meta: {
title: '列表',
permission: 'LIST', //用于筛选模块权限
module: '模块',
operations: [
{
key: 'ADD', // 用于自定义指令-按钮权限控制
name: '新增',
},
{
key: 'DELETE',
name: '删除',
},
]
}
6.按钮权限使用
<span
class="operation-btn"
v-operation:ADD
>新增</span
>
<span
class="operation-btn"
v-operation:DELETE
>删除</span
>
注意:1.权限改变后页面需要手动刷新,非响应式;
2.不建议在组件上使用自定义指令,在某些UI组件上使用就会失效例如(a-tabs);
3.前端筛选路由过于复杂,既要判断当前路由还要判断重定向路由,这部分逻辑也可由后端处理,之间返回生成好的路由表;