Vue3 实现模块级权限控制的最佳实践

Vue3 实现模块级权限控制的最佳实践

在这里插入图片描述

一、文章简介

在中大型前端项目中,权限控制是一项不可或缺的功能。无论是按钮级别、页面级别,还是模块级别的权限,合理的设计和高内聚的实现方式,直接影响系统的安全性和可维护性。

本文将详细讲解如何在 Vue3 项目中实现模块级权限控制,涵盖:

  • 权限模型设计
  • 动态路由控制
  • 指令封装实现按钮级别权限
  • 实战代码示例与业务抽象

二、权限模型设计

一个健壮的权限系统通常由以下三部分组成:

  • 用户信息:当前用户的角色、权限码、所属组织等信息
  • 权限码列表:由后端返回的权限标识集合(例如 ["user:add", "order:view"]
  • 资源权限映射:前端根据权限码决定哪些资源可见、可操作
// 示例:用户权限码
const userPermissions = [
  "dashboard:view",
  "user:edit",
  "order:create",
  "setting:view"
];

三、模块级权限控制核心逻辑

1. 动态路由过滤

动态根据用户权限加载对应路由模块:

// src/router/permission.ts
function hasPermission(route, permissions) {
    
    
  if (!route.meta || !route.meta.permission) return true;
  return permissions.includes(route.meta.permission);
}

function filterRoutes(routes, permissions) {
    
    
  return routes.filter((route) => {
    
    
    if (hasPermission(route, permissions)) {
    
    
      if (route.children) {
    
    
        route.children = filterRoutes(route.children, permissions);
      }
      return true;
    }
    return false;
  });
}

动态生成路由:

import {
    
     userStore } from '@/stores/user';
import {
    
     asyncRoutes } from './routes/async';

const permissions = userStore.getPermissions();
const allowedRoutes = filterRoutes(asyncRoutes, permissions);
router.addRoute(allowedRoutes);

每个路由配置中加上 meta.permission 字段:

{
    
    
  path: '/user',
  component: UserPage,
  meta: {
    
    
    permission: 'user:view'
  }
}

四、按钮级权限:自定义指令 v-permission

// directives/permission.ts
import {
    
     Directive } from 'vue';
import {
    
     userStore } from '@/stores/user';

export const vPermission: Directive = {
    
    
  mounted(el, binding) {
    
    
    const value = binding.value;
    const permissions = userStore.getPermissions();
    if (!permissions.includes(value)) {
    
    
      el.parentNode?.removeChild(el);
    }
  }
};

注册全局指令:

app.directive('permission', vPermission);

使用方式:

<el-button v-permission="'user:add'">新增用户</el-button>

五、模块级封装方案(推荐)

每个模块内部自带权限配置 + 路由文件:

// modules/user/router.ts
export default [
  {
    
    
    path: '/user',
    component: () => import('./views/UserList.vue'),
    meta: {
    
     permission: 'user:view' }
  }
];

统一在 modules/index.ts 中加载所有模块并过滤:

// modules/index.ts
import userRoutes from './user/router';
import orderRoutes from './order/router';

const allRoutes = [...userRoutes, ...orderRoutes];

export function getAuthorizedRoutes(userPermissions: string[]) {
    
    
  return filterRoutes(allRoutes, userPermissions);
}

六、前后端配合规范

建议后端提供统一的权限码接口,例如:

GET /api/user/info
{
    
    
  "username": "admin",
  "permissions": [
    "user:view",
    "user:add",
    "dashboard:view"
  ]
}

权限码建议使用统一命名规范:模块名:操作名,如:

  • user:add
  • user:view
  • order:export
  • dashboard:view

七、拓展内容:权限异常处理与提示

  • 当用户尝试访问未授权页面时,自动重定向至 403 页面
  • 通过路由守卫全局拦截非法访问
router.beforeEach((to, _, next) => {
    
    
  const permissions = userStore.getPermissions();
  if (to.meta.permission && !permissions.includes(to.meta.permission)) {
    
    
    return next('/403');
  }
  next();
});

八、动态菜单生成与权限联动渲染

菜单是权限系统的前端呈现核心,需根据权限码动态展示。

1. 从权限中生成菜单树

通常后端也会返回结构化菜单,带有 permission 字段,前端可以根据权限码过滤菜单:

function filterMenuTree(menuList, permissions) {
    
    
  return menuList
    .filter((item) => {
    
    
      if (!item.permission) return true;
      return permissions.includes(item.permission);
    })
    .map((item) => ({
    
    
      ...item,
      children: item.children
        ? filterMenuTree(item.children, permissions)
        : []
    }));
}

2. Vue 模板动态渲染菜单

<template>
  <el-menu>
    <template v-for="item in filteredMenu">
      <el-sub-menu v-if="item.children?.length" :index="item.path">
        <template #title>{
    
    {
    
     item.title }}</template>
        <el-menu-item
          v-for="child in item.children"
          :key="child.path"
          :index="child.path"
        >
          {
    
    {
    
     child.title }}
        </el-menu-item>
      </el-sub-menu>
      <el-menu-item v-else :index="item.path">{
    
    {
    
     item.title }}</el-menu-item>
    </template>
  </el-menu>
</template>

九、权限与页面缓存联动(keep-alive 配置)

如果页面权限变更后用户仍停留在旧页面,可能引发异常,需要通过 meta 配置控制缓存行为。

{
    
    
  path: '/setting',
  component: () => import('@/views/Setting.vue'),
  meta: {
    
    
    permission: 'setting:view',
    keepAlive: true
  }
}

统一封装 <KeepAlive> 包装器:

<template>
  <keep-alive :include="cacheablePages">
    <router-view v-slot="{ Component }">
      <component :is="Component" />
    </router-view>
  </keep-alive>
</template>

<script setup>
import {
    
     computed } from 'vue';
import {
    
     useRoute } from 'vue-router';

const cacheablePages = computed(() => {
    
    
  // 可结合权限码动态过滤
  return ['Setting', 'Dashboard'];
});
</script>

十、权限开发中常见问题与排查思路

问题 原因 排查建议
菜单不显示 权限码未匹配 打印后端权限码是否完整、格式是否一致
页面白屏 动态路由未注入成功 检查权限加载是否在路由加载之前完成
权限判断失效 自定义指令未生效 检查指令是否正确注册,全局权限数组是否响应式
页面缓存未更新权限 keep-alive 缓存未清除 手动通过 key 刷新组件或移除缓存

十一、前后端权限联动建议

  1. 接口级权限校验始终在后端进行,前端权限仅作辅助控制。
  2. 后端返回权限码应:
    • 统一命名风格
    • 提供接口查询权限(如 /api/user/permissions
    • 提供角色-权限配置页面以便运维人员使用
  3. 前端应保持权限逻辑与 UI 解耦,通过:
    • 路由 meta.permission
    • v-permission 指令
    • 动态菜单 permission 字段

十二、结语

权限系统是前端系统的核心保障模块之一。通过动态路由、按钮指令、菜单渲染、缓存控制等多个维度的设计与实践,Vue3 项目可实现灵活、安全、可维护的权限控制体系。