权限
前言
simple 模板项目采用了一种经典且安全的基于角色权限控制(RBAC)架构。系统实现了路由导航级与按钮组件级的多层级鉴权机制,并完全由后端动态分配的权限标识(Permission Codes)驱动。
权限系统简介
模板项目中的权限隔离可划分为两个核心层:
- 路由导航级控制:在全局路由守卫中拦截未授权访问,根据后端接口下发的菜单路由列表进行动态装载,使用户“只能访问其拥有权限的页面”。
- 按钮组件级控制:通过全局自定义指令(如
v-perms)精细化控制页面内某个操作按钮、卡片或表单项的显示与隐藏,阻止越权操作。
路由导航级控制
系统的路由鉴权逻辑完全集中在核心守卫 src/permission.ts 中:
全局前置守卫控制流
在 Vue Router 前置拦截器中,系统的权限校验流如图:
text
[ 路由开始跳转 (router.beforeEach) ]
│
┌─────────┴─────────┐
▼ ▼
[ 命中免登录白名单 ] [ 未命中免登录白名单 ]
│ │
放行 ▼
(next()) [ 检测 Token ]
┌───┴───┐
▼ ▼
[ 无 ] [ 有 ]
│ │
重定向到 ▼
登录页 [ 检测已获取用户信息 ]
┌───┴───┐
▼ ▼
[ 是 ] [ 否 ]
│ │
放行 ▼
[ 动态拉取权限并生成路由 ]
[ 递归添加动态路由 (addRoute) ]
│
重定向到目标页- 白名单校验:在进入路由前,守卫首先匹配
whiteList数组(例如登录页/login、忘记密码/forget等)。如果目标地址在白名单中,则无条件直接放行。 - 用户与路由数据延迟拉取:
- 如果用户已携带
token且尚未拉取用户信息,守卫将触发异步调用userStore.getUserInfo()。 - 接口返回当前用户的权限字符集
perms(来自于接口的data.privileges数组)和对应的后台菜单树。
- 如果用户已携带
- 异步路由过滤:
- 模板加载硬编码的菜单配置,通过
src/router/index.ts中的filterAsyncRoutes(menuRoutes, perms)进行过滤。 - 过滤函数会对每一条路由规则的
meta.perms进行校验,只有用户的perms集中包含了该路由的权限标识(或用户拥有通配符'*')时,该路由才会予以保留。
- 模板加载硬编码的菜单配置,通过
- 递归装载注册:
- 为了解决 Vue 项目中由于深层嵌套子路由(如三级及以上菜单)使用
keep-alive功能时出现缓存失效的问题,模板提供了addRoutesRecursively(routes)方法。 - 该方法递归地将符合权限的子路由提取出来,将路径拼接为绝对路径,并扁平化注册(调用
router.addRoute)到父级布局(INDEX_ROUTE_NAME)之下。
- 为了解决 Vue 项目中由于深层嵌套子路由(如三级及以上菜单)使用
- 重定向到第一个合法路由:
- 首次登录加载完毕后,通过
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 || ''
)
}绑定与配置新权限
若需在开发新功能时引入并配置一套全新的权限拦截:
- 后端注册权限字符:在管理后台的“菜单管理/权限管理”中,新增一条菜单或操作按钮记录,配置其对应的唯一权限编码(如
system.user/add)。 - 前端路由关联配置: 在编写前端静态菜单(如
src/config/menu/adminMenu.ts)时,将perms字段设置为该权限标识:ts{ path: 'system/user', title: '用户管理', icon: 'icon-user', perms: 'system.user/view', // 与后端配置的菜单权限字符一一映射 component: 'system/user/index' } - 按钮逻辑绑定:在对应的 Vue 组件中,对需要被鉴权的操作按钮绑定同样的权限字符即可:vue此后,只有后端对该角色分配了该权限,对应的页面菜单和操作按钮才会展示。
<u-button v-perms="['system.user/add']">新增用户</u-button>