权限

前言

simple 模板项目采用了一种经典且安全的基于角色权限控制(RBAC)架构。系统实现了路由导航级按钮组件级的多层级鉴权机制,并完全由后端动态分配的权限标识(Permission Codes)驱动。

权限系统简介

模板项目中的权限隔离可划分为两个核心层:

  1. 路由导航级控制:在全局路由守卫中拦截未授权访问,根据后端接口下发的菜单路由列表进行动态装载,使用户“只能访问其拥有权限的页面”。
  2. 按钮组件级控制:通过全局自定义指令(如 v-perms)精细化控制页面内某个操作按钮、卡片或表单项的显示与隐藏,阻止越权操作。

路由导航级控制

系统的路由鉴权逻辑完全集中在核心守卫 src/permission.ts 中:

全局前置守卫控制流

在 Vue Router 前置拦截器中,系统的权限校验流如图:

text
               [ 路由开始跳转 (router.beforeEach) ]

                    ┌─────────┴─────────┐
                    ▼                   ▼
            [ 命中免登录白名单 ]  [ 未命中免登录白名单 ]
                    │                   │
                  放行                  ▼
               (next())           [ 检测 Token ]
                                    ┌───┴───┐
                                    ▼       ▼
                                 [ 无 ]   [ 有 ]
                                    │       │
                                 重定向到    ▼
                                  登录页 [ 检测已获取用户信息 ]
                                            ┌───┴───┐
                                            ▼       ▼
                                         [ 是 ]   [ 否 ]
                                            │       │
                                          放行       ▼
                                               [ 动态拉取权限并生成路由 ]
                                               [ 递归添加动态路由 (addRoute) ]

                                               重定向到目标页
  1. 白名单校验:在进入路由前,守卫首先匹配 whiteList 数组(例如登录页 /login、忘记密码 /forget 等)。如果目标地址在白名单中,则无条件直接放行。
  2. 用户与路由数据延迟拉取
    • 如果用户已携带 token 且尚未拉取用户信息,守卫将触发异步调用 userStore.getUserInfo()
    • 接口返回当前用户的权限字符集 perms(来自于接口的 data.privileges 数组)和对应的后台菜单树。
  3. 异步路由过滤
    • 模板加载硬编码的菜单配置,通过 src/router/index.ts 中的 filterAsyncRoutes(menuRoutes, perms) 进行过滤。
    • 过滤函数会对每一条路由规则的 meta.perms 进行校验,只有用户的 perms 集中包含了该路由的权限标识(或用户拥有通配符 '*')时,该路由才会予以保留。
  4. 递归装载注册
    • 为了解决 Vue 项目中由于深层嵌套子路由(如三级及以上菜单)使用 keep-alive 功能时出现缓存失效的问题,模板提供了 addRoutesRecursively(routes) 方法。
    • 该方法递归地将符合权限的子路由提取出来,将路径拼接为绝对路径,并扁平化注册(调用 router.addRoute)到父级布局(INDEX_ROUTE_NAME)之下。
  5. 重定向到第一个合法路由
    • 首次登录加载完毕后,通过 findFirstValidRoute 找到第一个有效且未隐藏的路由节点,更新重定向路径,最后执行 next({ ...to, replace: true }) 进行重新导航。

按钮组件级控制

除了整个页面的访问权,对于页面内部敏感操作(如“添加商户”、“删除订单”等按钮),项目提供了全局自定义指令 v-perms 予以控制。

指令鉴权原理与超管放行

按钮级控制指令注册于 src/install/directives/perms.ts

ts
import useUserStore from '@/stores/modules/user'

export default {
  mounted: (el: HTMLElement, binding: any) => {
    const { value } = binding
    const userStore = useUserStore()
    const permissions = userStore.perms // 获取当前登录用户的权限代码列表
    const all_permission = '*' // 全局通配符,代表超级管理员权限

    if (Array.isArray(value)) {
      if (value.length > 0) {
        // 判断用户的权限数组里是否包含所需的权限代码,或者是否拥有超级管理员权限 '*'
        const hasPermission = permissions.some((key: string) => {
          return all_permission == key || value.includes(key)
        })

        // 如果未通过校验,则在 mounted 钩子中直接通过 DOM 操作物理移除该节点
        if (!hasPermission) {
          el.parentNode && el.parentNode.removeChild(el)
        }
      }
    } else {
      throw new Error('like v-perms="[\'auth.menu/edit\']"')
    }
  },
}

按钮级权限使用用例

在 Vue SFC 模板中,只需将所需的权限代码作为数组传入 v-perms 即可:

vue
<template>
  <div>
    <!-- 仅当用户拥有 'client.create' 权限或为超级管理员时,此按钮才会渲染和展示 -->
    <u-button v-perms="['client.create']" type="primary" @click="handleAdd">
      添加商户
    </u-button>

    <!-- 支持传入多个候选权限,用户满足其中任意一个即可显示 -->
    <u-button v-perms="['client.edit', 'client.delete']"> 管理商户 </u-button>
  </div>
</template>

权限校验辅助工具

在很多业务逻辑中,我们需要在脚本环境(而非模板环境)中进行权限判断。项目在 src/router/index.ts 中提供了以下辅助函数:

行内权限检查函数

hasPermission(perms, route) 函数常在计算属性或路由构建中使用:

ts
export function hasPermission(perms: any[], route: any) {
  if (route.meta && route.meta.perms) {
    // 判断用户的 perms 是否包含了 route 规则中声明的 perms
    return perms.some((key) => {
      return '*' == key || route.meta.perms.includes(key)
    })
  } else {
    // 如果路由元数据未配置 perms,默认所有人均可访问
    return true
  }
}

权限反查路由路径

getRoutePath(perms) 函数能根据权限字符,快速从当前运行路由表中查找到匹配该权限的页面路径:

ts
export function getRoutePath(perms: string) {
  const routerObj = useRouter() || router
  return (
    routerObj.getRoutes().find((item) => item.meta?.perms == perms)?.path || ''
  )
}

绑定与配置新权限

若需在开发新功能时引入并配置一套全新的权限拦截:

  1. 后端注册权限字符:在管理后台的“菜单管理/权限管理”中,新增一条菜单或操作按钮记录,配置其对应的唯一权限编码(如 system.user/add)。
  2. 前端路由关联配置: 在编写前端静态菜单(如 src/config/menu/adminMenu.ts)时,将 perms 字段设置为该权限标识:
    ts
    {
        path: 'system/user',
        title: '用户管理',
        icon: 'icon-user',
        perms: 'system.user/view', // 与后端配置的菜单权限字符一一映射
        component: 'system/user/index'
    }
  3. 按钮逻辑绑定:在对应的 Vue 组件中,对需要被鉴权的操作按钮绑定同样的权限字符即可:
    vue
    <u-button v-perms="['system.user/add']">新增用户</u-button>
    此后,只有后端对该角色分配了该权限,对应的页面菜单和操作按钮才会展示。