权限控制
前端项目一般有三种控制权限的方式:
- 通过用户角色来过滤菜单(前端方式控制),路由在前端配置,通过API返回角色过滤
- 通过后台来动态生成路由表(后台方式控制)
- 通过后台返回所有权限集合(包括菜单和按钮),前端固定路由,进行过滤
当前模板采用的是第三种权限控制方式。
路由权限控制
实现原理: 在前端固定路由表,并且配置路由所需的权限,实现权限的过滤。
缺点: 权限过多,不易维护,毕竟权限集合不在前端定义
优点: 简单,实用,菜单,按钮,等所有权限都可以通吃
路由模块
一个路由模块文件内容:
ts
import { RouteRecordRaw } from 'vue-router';
import Layout from '@/layouts/index';
export const fileRoutes: RouteRecordRaw[] = [
{
path: '/file',
name: 'file-root',
component: Layout,
redirect: '/file/pdf-preview',
meta: {
title: '文件预览',
icon: 'menu-file',
authCodes: ['MENU_FILE_PREVIEW'],
visible: true,
},
children: [
{
path: '/file/pdf-preview',
name: 'pdf-preview',
component: () => import('@/views/file/pdf-preview/index.vue'),
meta: {
authCodes: ['FC_PDF_PREVIEW'],
title: 'PDF预览',
},
},
{
path: '/file/cad-preview',
name: 'cad-preview',
component: () => import('@/views/file/cad-preview/index.vue'),
meta: {
authCodes: ['FC_CAD_PREVIEW'],,
title: 'CAD预览',
},
}
],
}
];
权限过滤
ts
// /src/store/permission
import { defineStore } from 'pinia';
import { RouteRecordRaw } from 'vue-router';
import { asyncRoutes, constantRoutes, catchAllRoutes } from '@/router';
import { intersection } from '@/utils/index';
/**
* 获取用户有权限的路由
* @param routes
* @param menus
*/
export function filterAsyncRoutes(routes: RouteRecordRaw[], authCodes: Array<any>): RouteRecordRaw[] {
const result: Array<RouteRecordRaw> = [];
routes.forEach((route: RouteRecordRaw) => {
const routeAuthCodes: string[] | any = route?.meta?.authCodes || [];
const matchedCodes = intersection(authCodes, routeAuthCodes);
if (matchedCodes.length > 0) {
if (route.children) {
route.children = filterAsyncRoutes(route.children, authCodes);
}
result.push(route);
}
});
return result;
}
type State = {
permissionRoutes: TypeObject[],
accessedRoutes: TypeObject[]
}
export const usePermissionStore = defineStore('_permission', {
state: (): State => ({
permissionRoutes: [], // 最终要展示到菜单的路由列表
accessedRoutes: [], // 匹配权限的菜单列表
}),
actions: {
generateRoutes(authCodes: Array<TypeObject>): Promise<TypeObject> {
return new Promise((resolve) => {
const accessedRoutes = filterAsyncRoutes(asyncRoutes, authCodes);
this.accessedRoutes = accessedRoutes;
this.permissionRoutes = [...constantRoutes, ...accessedRoutes, ...catchAllRoutes];
resolve({
accessedRoutes,
permissionRoutes: this.permissionRoutes,
});
});
},
},
});
路由注册
动态添加根据权限过滤的路由
ts
// src/router/index.ts
import {
createRouter, createWebHistory, RouteRecordRaw, RouteLocationNormalized,
} from 'vue-router';
import { isEmpty } from '@/utils/index';
import config from '@/config';
import { useUserStore } from '@/store/user';
import { usePermissionStore } from '@/store/permission';
import Layout from '@/layouts/index';
import Redirect from '@/layouts/redirect/index.vue';
import Page401 from '@/views/401.vue';
import Page404 from '@/views/404.vue';
import { homeRoutes } from './modules/home'; // 首页
import { tableRoutes } from './modules/table'; // 列表页
let userStore: TypeObject;
let permissionStore: TypeObject;
export const constantRoutes: Array<RouteRecordRaw> = [];
if (!config.sso) {
constantRoutes.unshift({
path: '/',
redirect: '/home',
meta: {
hidden: true,
},
});
}
export const catchAllRoutes: Array<RouteRecordRaw> = [
{
path: '/redirect',
component: Layout,
name: 'redirect',
meta: {
hidden: true,
},
children: [
{
path: '/redirect/:catchAll(.*)',
component: Redirect,
}
],
},
{
path: '/error',
name: 'error',
component: Layout,
redirect: '/error/404',
meta: {
hidden: true,
},
children: [
{
path: '/error/404',
name: '404',
component: Page404,
meta: {
hidden: true,
},
},
{
path: '/error/401',
name: '401',
component: Page401,
meta: {
hidden: true,
hideAside: true,
},
}
],
}
];
if (!config.sso) {
catchAllRoutes.push({
path: '/:catchAll(.*)',
redirect: '/error/404',
name: 'errorCatch',
meta: {
hidden: true,
},
});
}
export const asyncRoutes: Array<RouteRecordRaw> = [
...homeRoutes,
...tableRoutes
];
const router = createRouter({
history: createWebHistory(),
routes: config.sso
? [...constantRoutes, ...catchAllRoutes]
: [...constantRoutes, ...asyncRoutes, ...catchAllRoutes],
});
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: Func) => {
if (!userStore) {
userStore = useUserStore();
}
if (!permissionStore) {
permissionStore = usePermissionStore();
}
// 需要根据条件动态设置面包屑的时候可以在meta中设置setBreadcrumbs函数
if (typeof to.meta.setBreadcrumbs === 'function') {
to.meta.breadcrumbs = to.meta.setBreadcrumbs(to);
}
// 使用同一个页面组件的不同路由
const toModule = to.meta.module;
const fromModule = from.meta.module;
const isComponentReuse = !isEmpty(toModule) && toModule === fromModule;
if (config.sso) { // 使用集团通用的登录方式
const hasMenus = userStore.menus && userStore.menus.length > 0;
if (hasMenus) {
if (isComponentReuse) {
next(`/redirect${to.fullPath}`);
} else {
next();
}
} else {
const { authKeys } = await userStore.getUserInfo();
const { accessedRoutes, permissionRoutes } = await permissionStore.generateRoutes(authKeys);
console.log('根据用户权限动态生成的路由:', accessedRoutes);
// 设置用户输入 / 跳转至有权限的第一个路由
const firstRoute = permissionRoutes.filter((item: RouteRecordRaw) => !config.excludePath.includes(item.path) && item.meta)[0];
if (firstRoute) {
accessedRoutes.unshift({
path: '/',
redirect: firstRoute.path,
meta: {
hidden: true,
title: 'vue3模板示例',
},
});
}
// 动态添加路由
accessedRoutes.forEach((item: any) => {
router.addRoute(item);
});
// 配置404
router.addRoute({
path: '/:catchAll(.*)',
redirect: '/404',
meta: {
hidden: true,
},
});
// 用户没有菜单权限 进入401 用户无权限页面
if (!authKeys.length) {
if (config.excludePath.includes(to.path)) {
next();
return;
}
next('/error/401');
return;
}
next({ ...to, replace: true });
}
} else if (isComponentReuse) {
next(`/redirect${to.fullPath}`);
} else {
next();
}
});
router.afterEach((to: RouteLocationNormalized) => {
setTimeout(() => {
window.document.title = to.meta.title ? `${to.meta.title} - vue3模板示例` : 'vue3模板示例';
}, 0);
});
export default router;
细粒度权限控制
函数方式
使用方式:
vue
<template>
<section class="page-container">
<el-button
v-if="useCheckPermission(['ALARM_PAGE.U'])"
v-blur
type="primary"
>
添加
</el-button>
</section>
</template>
<script setup lang="ts">
// import { useCheckPermission } from '@/hooks/index'; // hooks中文件已被注入 hooks/index 中
// 或者
import { useCheckPermission } from '@/hooks/use-check-permission/index';
</script>
源码见src/hooks/use-check-permission/index
ts
/**
* @description: 检查用户是否具有某一个权限(只校验菜单和组件的code,不包含操作code)
* @param {string} code 校验权限的code
* 使用方式:
* useCheckPermission(['ALARM_PAGE'])
* useCheckPermission(['ALARM_PAGE.U'])
*/
import { useUserStore } from '@/store/user';
import { intersection } from '@/utils/index';
export const useCheckPermission = (authCodes: string[]): boolean => {
const userStore = useUserStore();
const authKeys: string[] = userStore.authKeys;
return intersection(authKeys, authCodes).length > 0;
};
指令方式
使用方式:
vue
<el-button v-permission="[FM_FILE.FC_CAD_PREVIEW.DEL]">删除</el-button>
或者
<el-button v-permission="FM_FILE.FC_CAD_PREVIEW.DEL">删除</el-button>
源码见src/directives/permission/index
ts
/**
* @description: 控制用户操作权限
*/
import { Directive } from 'vue';
import { useUserStore } from '@/store/user';
import { intersection } from '@/utils/index';
import { isPlainArray } from '@/utils/validate';
export const permission: Directive = {
mounted(el: any, binding: any) {
const userStore = useUserStore();
const authKeys: string[] = userStore.authKeys;
const { value } = binding; // 指令接收到的值
if (value && typeof value === 'string') {
const hasPermission = authKeys.includes(value);
if (!hasPermission) {
el.remove();
}
} else if (value && isPlainArray(value)) { // 指令是数组
const hasPermission = intersection(authKeys, value).length > 0;
if (!hasPermission) {
el.remove();
}
} else {
throw new Error("need roles! Like v-permission='[FM_FILE.FC_CAD_PREVIEW.R]' or v-permission='FM_FILE.FC_CAD_PREVIEW.R'");
}
},
};