Lazy loaded image
前端
Vue后台管理系统权限控制详解:从RBAC到动态路由与按钮级权限实
字数 9982阅读时长 25 分钟
2024-5-23
2025-5-9
type
status
date
slug
summary
tags
category
icon
password

前言:权限控制的重要性

在构建后台管理系统时,权限控制与安全性是项目的基石,必须在项目初期就进行深入思考和搭建。一个完善的权限系统能够确保不同用户在系统中拥有恰当的操作权限,保护敏感数据和功能不被未授权访问。

1. 权限控制方案选型:为何选择RBAC?

后台管理系统的权限控制有多种实现方案,每种方案都有其特点和适用场景:
权限方案类型
描述
基于角色的访问控制(Role-Based Access Control,RBAC)
推荐方案。系统定义不同角色,每个角色关联一组权限。用户被分配一个或多个角色,通过角色间接获得权限。管理灵活,易于维护。
基于权限的访问控制(Permission-Based Access Control)
将权限直接分配给用户,不通过角色。每个用户拥有独立的权限列表,控制其对功能和资源的访问。
基于资源的访问控制(Resource-Based Access Control)
权限控制与资源本身关联。每个资源定义自身的访问权限,用户通过被授予特定资源的访问权限来操作资源。
层次结构权限控制(Hierarchical Access Control)
基于资源和操作的层级结构进行权限控制。资源和操作组织成树状结构,用户被授予访问某个层级及其子层级的权限。
基于规则的访问控制(Rule-Based Access Control)
使用预定义的规则(可基于用户属性、环境条件等)来动态确定用户对功能和资源的访问权限。
经过综合考量,我选择了基于角色的访问控制(Role-Based Access Control,RBAC)。RBAC是目前前端权限控制最主流、最灵活且易于管理的方式,它能清晰地定义用户、角色和权限之间的关系。
在RBAC模型中:
  • 用户 (User):系统的操作者。
  • 角色 (Role):权限的集合,代表了一组操作许可。
  • 权限 (Permission):对特定资源或功能的操作许可。
管理员只需管理角色及其权限,然后将角色分配给用户,极大地简化了权限管理的复杂性。当需要调整用户权限时,只需修改其所属角色的权限即可,无需对每个用户进行单独设置。RBAC还支持灵活的权限组合和良好的扩展性。

2. RBAC模型下的权限字段设计与管理

2.1. 用户权限授权核心概念

用户权限授权是在用户身份认证通过后,对用户访问系统内菜单、按钮等资源的进一步控制。
  • Permission (权限标识/权限字符串):定义对系统特定资源的访问许可,例如 sys:user:add (系统模块-用户管理-新增操作)。
  • Role (角色):权限的集合,一个角色可以包含多个权限标识,从而决定了该角色用户可以访问哪些菜单、执行哪些操作。
权限字符串命名规范与校验规则:
  • 命名规范:推荐采用 模块:功能:操作 的格式,例如 sys:user:edit
    • 使用冒号 : 分隔,清晰划分资源层级。
    • 例如:sys:user:edit 代表 系统模块:用户功能:编辑操作
  • 匹配规则
    • 精确匹配:用户拥有的权限字符串必须与受保护资源所需的权限字符串完全一致。
    • 通配符匹配(建议谨慎使用,尤其在前后端分离场景):
      • sys:user:* 或简化为 sys:user,可以匹配以 sys:user:sys:user 开头的所有权限。
      • sys 可以匹配以 sys:sys 开头的所有权限。
  • 命名规范的优势
      1. 可读性与可理解性:直观表达权限含义,方便开发和管理人员理解。
      1. 可扩展性与灵活性:便于新增模块、功能或操作,无需修改现有规则。
      1. 细粒度控制:支持对特定功能和操作的精确权限管理。
      1. 避免冲突:通过模块、功能、操作的组合确保权限名称的唯一性,防止混淆。

2.2. 核心数据权限管理模型

关键数据模型及其关系如下:
  • 用户 (User):包含登录账号、密码、关联的角色列表。
  • 角色 (Role):包含角色名称、角色标识符(可选)、关联的菜单/权限列表。
  • 菜单/权限 (Menu/Permission):包含菜单名称、路由URL/权限标识、菜单类型(目录、菜单、按钮)等。
  • 用户-角色关系 (User-Role):多对多关系,存储用户ID和角色ID的映射。
  • 角色-菜单/权限关系 (Role-Menu/Permission):多对多关系,存储角色ID和菜单/权限ID的映射。
关系图:

3. 前端权限实现思路与步骤(路由级权限优先)

前端权限通常分为路由级权限(控制页面访问)和按钮级权限(控制页面内元素操作)。我们先实现路由级权限。
整体流程图:
上图展示了用户从 登录 -> 路由守卫 -> 权限验证 -> 构建动态路由表 -> 跳转目标页面 的正向流程。其中,权限验证构建动态路由表 是在 路由守卫 中执行的关键步骤。
开发准备阶段需考虑:
  1. 与后端明确权限相关字段(如角色列表、权限标识列表)及其数据类型。
  1. 确定动态路由表的生成方:前端生成还是后端生成。
下面我们按照流程逐步实现。

3.1. 步骤一:用户登录与Token处理

用户登录成功后,后端返回 token。前端将 token 存储到本地(如 sessionStoragelocalStorage)。
登录成功后,进行路由跳转到首页。
优化:记录登录前访问的页面 如果希望用户在 token 失效后重新登录能返回到之前的页面,可以在路由守卫中拦截未授权访问时,将目标路径 to.fullPath 作为 redirect 查询参数存到登录页路由。
在登录页 login.vue 中获取并使用 redirect 值:

3.2. 步骤二:路由守卫与初步权限校验

当用户进行路由跳转时,会触发Vue Router的导航守卫(beforeEach)。
路由守卫核心逻辑:
  1. 检查Token
      • token
        • 目标路由是否在白名单(如登录页、404页)内?
          • 是:放行 next()
          • 否:重定向到登录页,并携带 redirect 参数 next({ path: '/login', query: { redirect: to.fullPath } })
      • token
        • 目标路由是否是登录页?
          • 是:已登录用户无需再访问登录页,重定向到首页 next({ path: '/' })
          • 否:进入下一步,校验用户角色和权限。
  1. 校验用户角色信息
      • 检查Vuex(或其他状态管理)中是否已存在当前用户的角色信息。
      • 若无,则需要调用接口获取用户信息(包含角色和权限列表)。

3.3. 步骤三:获取用户角色与权限信息

调用后端接口获取当前登录用户的详细信息,其中应包含用户的角色列表 (roles) 和权限标识列表 (permissions)。
根据前文设计的RBAC模型,rolespermissions 都是数组类型。权限标识 permission 的格式规范已在上文提及。
后端返回的用户信息示例:
notion image
notion image

3.4. 步骤四:动态路由表的生成方抉择

获取到用户角色和权限后,需要根据这些信息生成用户可访问的动态路由表。此时面临一个关键决策:动态路由表由前端生成还是后端生成?
方案对比:
  1. 后端提供路由表
      • 优点
        • 安全性高:权限控制的核心逻辑在后端,更难被绕过。
        • 隐藏敏感信息:后端可以根据权限过滤掉不应暴露给前端的路由结构。
        • 适用于复杂规则:后端能实现更复杂的权限判断逻辑。
      • 缺点
        • 前后端耦合度高:前端路由结构依赖后端,沟通成本增加。
        • 前端开发受限:可能需等待后端提供路由表才能进行部分开发测试。
  1. 前端提供路由表(基于权限过滤)
      • 优点
        • 前后端解耦:前端可独立定义完整路由表,根据权限动态过滤。
        • 用户体验可能更灵活:前端动态构建菜单和路由,响应更快。
      • 缺点
        • 安全性相对较低:前端代码暴露,理论上路由结构可被探知(但实际访问仍需后端接口校验)。
        • 敏感信息隐藏困难:需严格依赖后端接口的授权。
考虑到后台管理系统对安全性的高要求,推荐由后端根据用户权限生成并返回该用户可访问的路由表信息(通常是路由配置的JSON数据)
注意一个常见问题:后端返回的路由表(通常是JSON对象)不能直接被Vue Router使用,因为前端路由组件通常使用动态导入 () => import('@/views/xxx.vue') 实现懒加载,这是一个函数,而JSON不支持函数。我们将在后续步骤解决此问题。

3.5. 步骤五:定义公共路由与处理动态路由

我们将应用的路由分为两部分:
  • 公共路由 (Constant Routes):所有用户(无论角色和登录状态)都可以访问的路由,如登录页、404页、403页、以及一些固定的布局路由(如首页框架)。这部分路由在前端项目中预先定义好。
    • 注意:Vue Router 4.x 中,对于404页面的通配符路由写法是 { path: '/:pathMatch(.*)*', component: ... }
  • 动态路由 (Async/Private Routes):根据用户权限由后端返回的、需要动态添加到Vue Router实例中的路由。 用户最终的完整路由表 = 公共路由 + 动态路由。后端返回的原始路由数据示例: 注意后端返回的 component 字段是字符串。
    • 处理后端返回的动态路由: 如前所述,后端返回的路由配置中 component 字段通常是组件路径字符串(如 "Layout", "views/sys/user/index"),需要前端将其转换为实际的组件加载函数。
      后端返回的路由对象示例 (JSON)
      前端转换逻辑需要考虑
      1. component 字符串到动态导入函数的映射。
      1. 路由结构符合Vue Router的要求。
      1. 支持自定义 meta 属性(如菜单图标、页面标题、权限标识、缓存策略等)。
      前端定义的一个动态路由对象结构可能如下(转换后)
      notion image
      notion image
      notion image
      关于前端向后端传递路由配置的疑问: 在RBAC方案中,前端通常不直接传递“路由表”给后端。而是通过“用户管理”、“角色管理”、“菜单/权限管理”这些后台功能模块,让管理员在UI上配置用户的角色、角色的权限(关联到哪些菜单/操作)。这些配置信息被保存到后端数据库。当用户登录时,后端根据这些已存的配置动态生成并返回该用户专属的路由表。
notion image

3.6. 步骤六:RBAC的权限配置界面(管理员视角)

为了实现RBAC,后台管理系统需要提供相应的管理界面:
  • 菜单(权限)管理:用于定义系统中的所有页面路由和操作权限。通常以树形表格结构展示,方便管理层级关系。 在这个界面中,可以配置路由的各个属性,如:
    • notion image
    • 路由地址 (path):同层级下必须唯一。
    • 组件路径 (component):填写相对于 src/views/ (或约定目录) 的路径字符串,例如 sys/dept/index。前端在转换时会自动拼接成 () => import('@/views/sys/dept/index.vue')
    • 如果填写的组件路径不存在,转换逻辑中应有错误处理(如 .catch(() => import('@/views/error-page/404.vue'))),避免白屏。 配置好新的菜单/权限后,这些信息会提交给后端保存。后端会根据这些信息更新其内部的路由定义。
      • notion image
  • 角色管理:用于创建和管理角色,并将“菜单/权限”分配给角色。
    • notion image
      notion image
  • 用户管理:用于创建和管理用户,并将“角色”分配给用户。

3.7. 步骤七:获取并转换动态路由(核心逻辑)

这是前端权限控制中最核心的一步。在 src/router (或类似目录)下创建 generatorRouters.js (或 asyncRoutes.js)。
generatorDynamicRouter 方法解析: 此方法负责调用后端API获取原始路由配置。因为涉及到网络请求,所以它返回一个 Promise
formatRouterTree 方法解析 (核心递归转换): 此方法是关键,它递归遍历后端返回的路由树结构,将每个节点转换为符合Vue Router要求的路由对象:
  • component 转换:将字符串路径(如 "sys/user/index")转换为 () => import('@/views/sys/user/index.vue') 这样的动态导入函数。同时处理组件路径不存在或加载失败的情况,通常会重定向到404页面。
  • path 处理:确保子路由的路径正确拼接(通常相对于父路由)。
  • meta 信息处理:将后端返回的 meta 数据(如标题、图标、权限标识等)赋给前端路由对象的 meta 字段。
  • 递归处理 children
转换前后的路由对象对比:
转换前 (后端JSON):
是字符串。
notion image
转换后 (前端Vue Router对象):
是一个函数。
notion image

3.8. 步骤八:存储格式化路由及导航菜单信息(Vuex示例)

获取并转换好的动态路由表需要存储起来,通常使用Vuex(或Pinia等状态管理库)。同时,这份路由表也是生成侧边栏导航菜单的数据源。

3.9. 步骤九:在路由守卫中添加动态路由

回到路由守卫 (src/router/permission.jsmain.js 中配置的守卫),在获取到用户信息和角色后,调用 GenerateRoutes action,然后将生成的动态路由添加到Vue Router实例中。
重要:在Vue Router 4.x中,router.addRoute() 用于动态添加单个路由对象。如果有一组路由,需要遍历并逐个添加。添加后,必须使用 next({ ...to, replace: true }) 来重新发起导航,否则新添加的路由可能不会立即生效,导致白屏(详见后面的“坑”)。

3.10. 步骤十:用户登出处理

用户登出时,需要:
  1. 调用后端登出接口。
  1. 清除前端存储的 token
  1. 重置Vuex中的用户信息(token, roles, permissions)、动态路由等。
  1. 清空本地缓存的相关数据(如 sessionStorage)。
  1. 重置Vue Router实例,移除动态添加的路由(防止下一个登录用户看到不属于自己的路由)。
  1. 重定向到登录页。
注意resetRouter() 的实现方式有多种,一种是重新创建Router实例并替换 matcher (Vue Router 4.x推荐),另一种是遍历并使用 router.removeRoute(routeName) 移除动态添加的路由。
至此,路由级别的权限控制流程基本完成。

4. 按钮级权限控制

按钮级权限(或更广义地,页面内元素的显隐控制)确保用户只能看到和操作其拥有权限的按钮、字段、组件等。
主流实现方式:
  1. 条件渲染 (v-if / v-show):基于用户的权限列表 (permissions) 和元素所需的权限标识,在模板中使用 v-ifv-show 控制元素的渲染或显示。
  1. 自定义指令 (Custom Directive):封装权限检查逻辑到自定义指令中,如 v-hasPermi="['sys:user:add']"
  1. 全局方法/组件封装:提供一个全局方法或组件来进行权限检查。
本文主要介绍自定义指令和全局方法的方式,它们都依赖于用户登录后获取的 permissions 列表(权限标识数组)。
回顾 permissions 字段:
它在获取用户信息时从后端返回,并存储在Vuex中。
notion image
notion image

4.1. 自定义指令实现按钮级权限 (v-hasPermi)

创建一个自定义指令 v-hasPermi,用于检查用户是否拥有指定的操作权限。
全局注册指令 (main.js):
在组件中使用:
如果用户权限列表中不包含 system:user:add,则“新增用户”按钮会被隐藏或移除。

4.2. 全局方法实现按钮级权限 ($checkPermi)

对于需要在JavaScript逻辑中(而非模板中)判断权限的场景,可以封装一个全局方法。
挂载到Vue全局属性 (main.js):
在组件的 <script setup> 或 Options API 中使用:

5. 实现过程中可能遇到的坑与解决方案

  1. Vue Router 4.x addRoute vs Vue Router 3.x addRoutes
      • Vue 3 (Router 4.x) 使用 router.addRoute(routeObject) 添加单个路由。
      • Vue 2 (Router 3.x) 使用 router.addRoutes(routesArray) 添加路由数组。
      • 两者在参数类型和处理已存在同名/同path路由时的覆盖逻辑不同。
  1. 调试动态添加的路由
      • 使用 router.addRoute() 后,直接在控制台打印 router.getRoutes() 可能不会立即显示新添加的路由,或者在Vue Devtools中也可能未及时更新。这是因为 addRoute 的内部处理可能是异步的,并且路由的匹配器更新也需要一个过程。
      • 关键:确保在 addRoute 之后使用 next({ ...to, replace: true }) 来强制重新导航,这样新的路由才能被正确匹配。
  1. next({ ...to, replace: true }) 的重要性
      • 动态添加路由后,如果不使用 next({ ...to, replace: true }),直接 next()next(to.path),可能会导致白屏。因为此时路由系统可能还未完全识别新添加的路由。replace: true 确保当前导航历史被替换,避免用户回退到无效状态。
  1. 用户登出后,新用户访问到旧用户路由的问题
      • 原因:用户登出时,如果仅清除了 token 和 Vuex 状态,Vue Router 实例中动态添加的路由依然存在。
      • 解决方案:在用户登出逻辑中,必须重置路由。
    1. Vuex状态在页面刷新后丢失的问题
        • 原因:Vuex存储在内存中,页面刷新会导致内存清空,Vuex状态丢失。
        • 解决方案:使用持久化存储插件(如 vuex-persistedstate)或手动在页面加载和卸载时将Vuex状态与 localStorage / sessionStorage 同步。
          src/router/index.js 中的本地路由缓存逻辑示例 (用于刷新时减少API调用,可选):

      6. 总结与后续优化方向

      本文详细阐述了在Vue后台管理系统中,基于RBAC模型设计和实现前端权限控制的完整流程,包括路由级权限和按钮级权限。通过动态生成路由、路由守卫拦截、自定义指令/全局方法等技术,构建了一个相对完善的权限体系。
      可进一步优化的点:
      1. 代码封装与抽象:将路由守卫中的权限验证、动态路由生成与添加逻辑进一步封装,提高可维护性。
      1. 开发环境路由:为开发环境准备一套包含所有路由的配置,方便前端独立开发调试,无需依赖后端权限。
      1. 侧边导航栏生成:本文未详述,但格式化后的动态路由数据是生成导航菜单的直接数据源,可根据需要进一步处理(如过滤隐藏项、排序等)。
      1. 性能考虑
          • 本地缓存路由:如上文所述,将从后端获取的原始路由配置(或格式化后的路由)存入 sessionStorage,刷新页面时优先从缓存加载,减少API请求,提升体验。但需注意缓存策略与安全性(路由结构暴露、更新问题)。
          • API请求优化GetInfo(获取用户信息)和 GenerateRoutes(获取并生成路由)这两个请求可以考虑是否能合并或优化其调用时机。
      权限控制是后台系统的核心,合理的方案设计和细致的实现对系统安全和用户体验至关重要。希望本文能为你提供有价值的参考。
      上一篇
      Nginx从入门到精通:高性能负载均衡、反向代理与核心功能实战指南
      下一篇
      Vue实战:下拉框拼音搜索性能优化秘籍 (pinyin-pro与缓存策略)