外观
权限集成
概述
前端权限体系围绕「登录拿到令牌 → 拉取权限信息 → 生成可访问菜单/路由 → 页面与按钮级鉴权」这条主线展开。它建立在 Vben Admin 的 @vben/access、@vben/stores 能力之上,并与后端的 RBAC(角色-菜单-权限)模型天然对接。
权限信息分为三层:
- 菜单/路由权限:决定用户能看到哪些页面、能进入哪些路由。
- 角色(roles):用户拥有的角色标识集合,用于前端路由过滤(frontend 模式)与展示。
- 权限码(permissions):形如
模块:资源:操作(如system:user:create)的细粒度字符串,用于按钮级鉴权。
权限信息的下发流程
登录成功后,前端通过一次接口调用把「用户 + 角色 + 权限码 + 菜单」一并拉回,并写入对应的 Pinia Store,后续鉴权全部基于这些本地缓存,不再频繁请求后端。
核心接口与数据来源:
- 登录:
POST /system/auth/login,返回令牌(见apps/web-ele/src/api/core/auth.ts)。 - 权限信息:
GET /system/auth/get-permission-info,由getAuthPermissionInfoApi()封装,返回结构即AuthPermissionInfo:
ts
interface AuthPermissionInfo {
user: UserInfo; // 用户信息,含 homePath 首页地址
roles: string[]; // 角色标识集合
permissions: string[]; // 权限码集合
menus: AppRouteRecordRaw[]; // 后端下发的菜单(路由元信息)
}写入逻辑位于 apps/web-ele/src/store/auth.ts 的 fetchUserInfo 中:
ts
userStore.setUserInfo(authPermissionInfo.user);
userStore.setUserRoles(authPermissionInfo.roles);
accessStore.setAccessMenus(authPermissionInfo.menus);
accessStore.setAccessCodes(authPermissionInfo.permissions);
accessToken、refreshToken、accessCodes等会通过 Pinia 持久化插件落地到本地存储,刷新页面后无需重新登录即可恢复鉴权上下文。
相关的状态 Store
| Store | 关键字段 / 方法 | 作用 |
|---|---|---|
useAccessStore (core-access) | accessToken、accessCodes、accessMenus、accessRoutes、isAccessChecked | 存放令牌、权限码、可访问菜单与路由,及是否已生成过动态路由 |
useUserStore (core-user) | userInfo、userRoles | 存放用户资料与角色标识 |
这两个 Store 都来自 @vben/stores,业务代码中可直接引入:
ts
import { useAccessStore, useUserStore } from '@vben/stores';路由与菜单的生成
菜单与路由的生成发生在路由守卫中,由 apps/web-ele/src/router/guard.ts 的 setupAccessGuard 驱动:
- 校验
accessToken,缺失则跳转登录页(带redirect回跳地址)。 - 若
isAccessChecked已为true,直接放行,避免重复生成。 - 调用
fetchUserInfo拉取最新权限信息(若尚未加载)。 - 调用
generateAccess({ roles, router, routes })生成可访问的菜单与路由。 - 将结果写入
accessStore,并把isAccessChecked置为true,最后重定向到首页或目标页。
生成策略由偏好配置 accessMode 决定(项目默认值为 backend,见 apps/web-ele/src/preferences.ts):
- backend:最常用。前端仅维护一份基础路由骨架,页面菜单由后端的
get-permission-info返回,权限集中在后端配置。 - frontend:路由在前端写死(在
meta.roles上声明可见角色),适合菜单结构固定、仅做角色过滤的场景。 - mixed:二者结合,后端菜单为主、前端路由补充。
若某路由
meta.menuVisibleWithForbidden = true,它会在菜单中可见,但访问时会被重定向到 403 页面(forbiddenComponent)。
按钮级鉴权
路由权限解决了「能不能进页面」,按钮权限解决「页面里能不能点」。框架提供三种方式,按场景选择。
useAccess 组合式函数
在 <script setup> 中通过 useAccess() 获取鉴权方法:
ts
import { useAccess } from '@vben/access';
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
// 判断当前用户是否拥有指定权限码(任一满足即可)
const canCreate = hasAccessByCodes(['system:user:create']);
// 判断当前用户是否拥有指定角色
const isAdmin = hasAccessByRoles(['ADMIN']);hasAccessByCodes 与 hasAccessByRoles 都是「传入集合中存在任一匹配即返回 true」的或逻辑,便于多个权限/角色共用一个入口。
v-access 指令
用于整块 DOM 的显隐控制:无权限时该元素会被直接从 DOM 中移除。指令在 bootstrap.ts 启动时全局注册,使用时无需 import。
语法:
vue
<!-- 按权限码控制(backend 模式的主力用法) -->
<el-button v-access:code="['system:user:create']">新增</el-button>
<!-- 按角色控制(仅 frontend 模式生效;backend 模式下 role 也会走权限码判断) -->
<el-button v-access:role="['ADMIN']">管理</el-button>v-access 接收一个数组或单个字符串,值为权限码或角色标识。典型业务页面(如 views/infra/demo/general/demo03)中的新增、导出、删除按钮即采用此写法:
vue
<el-button v-access:code="['infra:demo03-student:create']">新增</el-button>
<el-button v-access:code="['infra:demo03-student:export']">导出</el-button>
<el-button v-access:code="['infra:demo03-student:delete']">删除</el-button>AccessControl 组件
当需要在「有权限时渲染某些内容、无权限时渲染兜底内容」的场景下使用,适合配合 <template #default> 与 <template #fallback> 插槽(或条件渲染):
vue
<script setup lang="ts">
import { AccessControl } from '@vben/access';
</script>
<template>
<AccessControl :codes="['system:user:update']" type="code">
<el-button>编辑</el-button>
</AccessControl>
</template>type 可选 code(默认)或 role,codes 为权限码/角色数组。
权限码命名约定
项目统一采用 模块:资源:操作 三段式,例如:
system:user:create—— 系统模块的「用户」资源的「新增」操作infra:demo03-student:export—— 基础设施演示模块的「学生」资源导出
这套命名与后端 RBAC 的权限标识完全一致:后端在 get-permission-info 中返回用户所拥有的权限码集合,前端只需用 hasAccessByCodes 或 v-access:code 做包含判断即可,前后端无需额外映射。
开发实践建议
- 页面入口用路由权限,页内操作按钮用权限码:菜单由后端统一收口,按钮通过
v-access:code声明,二者职责清晰。 - 优先使用指令而非手动
v-if:v-access会自动移除无权限节点,避免残留不可点击的灰色按钮暴露业务结构。 - 不要在前端做「唯一」的安全校验:前端鉴权只解决体验问题(隐藏/禁用),真正的越权拦截必须由后端接口层的权限校验保证。
- 新增菜单需在后端配置:在 backend 模式下,没有在后端菜单中登记的页面即使写好了前端代码,也不会出现在可访问路由里。
- 切换账号后注意重置:登录与登出都会通过
resetAllStores清理 Store,确保不同权限的用户之间不会串号。