vue3+ts+vuex+自定义指令实现权限控制

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.前端筛选路由过于复杂,既要判断当前路由还要判断重定向路由,这部分逻辑也可由后端处理,之间返回生成好的路由表;
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大兵的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值