Skip to content

权限控制

前端项目一般有三种控制权限的方式:

  1. 通过用户角色来过滤菜单(前端方式控制),路由在前端配置,通过API返回角色过滤
  2. 通过后台来动态生成路由表(后台方式控制)
  3. 通过后台返回所有权限集合(包括菜单和按钮),前端固定路由,进行过滤

当前模板采用的是第三种权限控制方式。

路由权限控制

实现原理: 在前端固定路由表,并且配置路由所需的权限,实现权限的过滤。

缺点: 权限过多,不易维护,毕竟权限集合不在前端定义

优点: 简单,实用,菜单,按钮,等所有权限都可以通吃

路由模块

一个路由模块文件内容:

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'");
    }
  },
};

内容由donymh提供,如有疑问,请微信联系lmingh6