权限设计模式【前端 RBAC】
权限设计模式(前端 RBAC:运行时配置 + 路由角色准入 + 按钮级权限点)
本文基于一个真实的中后台项目落地经验,总结一套“企业级可演进”的前端权限设计模式:在后端暂时无法改造的前提下,用前端 RBAC 把“能不能进模块 / 菜单显示 / 按钮能不能点 / 弹窗能不能提交”统一收敛到一套可配置、可扩展、可测试的体系里。
本文既是设计思路讲解,也给出可直接复制的实现范式(本仓库的实现为 React 版本;后续将补充 Vue/React 的可运行示例代码目录)。
0. 先说结论:这套模式解决了什么
在中后台里,权限通常会分散在三类地方:
- 路由:能不能进某个模块
- 菜单:能不能看到某个入口
- 页面:按钮/操作列/表单是否可操作
如果你把“权限”只做成 if (roles.includes('ops')) 之类的硬编码,随着角色增加(例如 ops-viewer、ops-auditor、finance…),你会面临:
- 判断散落在项目各处,难以统一修改
- 新增角色时需要全局搜改,回归风险巨大
- 读写权限难以表达(只读角色常见)
- 前端体验与后端鉴权容易脱节(但后端又不一定能及时配合)
本文介绍的模式的关键点是:
- 路由级:仍可保留“角色数组”作为准入(快速、粗粒度、能控住模块边界)
- 按钮级:引入“权限点 code”(细粒度动作能力),统一用
useCan()/AuthButton判断 - 配置级:把
role -> permissions映射放到rbac.json里,支持运行时加载与回退,做到“改配置即可调权限” - 可演进:未来后端可改造时,可以把权限点下发到
/me,前端接入点不变,只替换数据源
重要声明:前端权限不是安全边界。它的核心目标是统一体验、减少误操作、降低维护成本;任何写接口的安全性必须由后端鉴权兜底。
1. 权限的分层模型(企业级推荐)
1.1 三层权限:路由 / 菜单 / 动作
在工程实践中,把权限明确分层可以显著降低复杂度:
- 路由准入(Route Guard):控制“能不能进入某个模块/页面”
- 菜单可见(Menu Visibility):控制“能不能看到入口”
- 动作权限(Action Permission):控制“按钮是否可点、提交是否允许、操作列是否可用、表单是否只读”
其中:
- 路由准入更适合使用“角色数组”判断(粗粒度)
- 动作权限更适合使用“权限点 code”判断(细粒度)
- 菜单可见通常跟路由准入一致,但在一些企业场景会“可见但不可进”或“可进但不可见”(例如直接跳转链接),所以最好独立处理
1.2 RBAC 的正确抽象:Role -> Permission
推荐不要在业务代码里写“角色判断”,而是统一写“权限点判断”:
- Role(角色):一类人(ops、ops-viewer)
- Permission Code(权限点):一个动作能力(customer.update、recharge.write)
- Role-Permission Mapping:角色拥有哪些权限点(可配置)
这样你新增角色时,只需要维护“角色拥有哪些权限点”,而不是全项目搜改 if/else。
2. 本项目的落地结构(工程视角)
2.1 文件结构(核心入口)
这套模式在本仓库的关键文件如下(此为原项目目录,非示例代码结构):
- 运行时配置文件:
public/rbac.json - 配置加载与兜底:
src/store/authz.ts - 权限匹配核心(支持 * 通配):
src/authz/rbac.ts - React Hook 入口:
src/authz/useCan.ts - 按钮封装组件:
src/authz/AuthButton.tsx - 路由守卫(同时负责拉取 /me):
src/router/AuthRoute.tsx - 路由表(模块准入用 roles 数组):
src/router/index.tsx - 菜单过滤(模块入口可见性用 roles 数组):
src/layouts/BasicLayout.tsx
2.2 运行时数据流(从登录到按钮权限)
一个典型的页面进入流程可以抽象成:
登录成功 -> token 持久化
|
进入受保护路由(AuthRoute)
|
1) 拉取 /v1/oauth/me 得到 user + roles(后端返回)
2) 拉取 /rbac.json 得到 role->permissions(静态配置)
|
路由级:handle.permissions(roles)决定能否进入
按钮级:useCan/AuthButton(permission code)决定可见/可点/可提交
注意:这套前端 RBAC 的实现不要求后端返回 permissions,只要后端能返回 roles(哪怕只是一个 ops-viewer),前端就能用本地映射推导出动作能力。
3. 配置:rbac.json 怎么写、怎么匹配
3.1 配置文件格式(role -> permissions)
public/rbac.json 的结构(示意):
{
"version": 1,
"roles": {
"guest": { "permissions": [] },
"ops-viewer": { "permissions": ["operations.*.read"] },
"ops": { "permissions": ["operations.*"] },
"super": { "permissions": ["*"] }
}
}
字段含义:
version:版本号(目前用于校验;未来可做灰度/兼容)roles:以角色为 key 的对象roles[role].permissions:该角色拥有的权限点/通配模式
3.2 通配符规则(非常关键)
本项目支持 * 通配符,语义是“任意字符任意长度(包含 .)”,并采用全字符串匹配。
举例:
operations.*匹配operations.customers.update、operations.customers.recharge.writeoperations.*.read匹配operations.customers.read、operations.customers.pricing.read*匹配所有权限点(通常只给super)
实现入口:src/authz/rbac.ts 中的 matchPermission()。
3.3 为什么用“运行时配置 + 兜底”
运行时配置最大的价值是:你可以不改 TS 代码,只改一个静态 JSON 即可调整权限,非常适合:
- 后端暂时无法配合(常见于跨团队依赖、排期卡点)
- 需要快速 A/B 验证或紧急调整权限策略
- 希望测试能自己切换“只读/可写”角色验证体验
兜底策略也很重要:
/rbac.json加载失败:不要阻断页面渲染(否则会造成全站不可用)- 配置不合法:回退到内置默认配置(
defaultRbacConfig)
实现入口:src/store/authz.ts。
4. 权限点(Permission Code)怎么设计:命名规范与边界
4.1 命名规范(建议)
推荐使用层级命名,便于通配与治理:
{domain}.{resource}.{action}[.{scope}]
例如:
operations.customers.read(读)operations.customers.create(新增)operations.customers.update(编辑)operations.customers.recharge.read(查看充值)operations.customers.recharge.write(充值写入)
为什么要区分 read/write:
- 只读角色(如
ops-viewer)非常常见 - 用
*.read的通配能一键覆盖整个域的“查看能力” - 写权限更容易被误授,需要更慎重、颗粒度更细
4.2 权限点边界(不要过细,也不要过粗)
企业级实践建议:
- 先按“模块/资源”划分:
operations.customers.* - 再按“动作类型”划分:
read/create/update/delete/… - 需要表达“写能力集合”时,引入
*.write(聚合多个写动作)
不要一开始就过度细化到字段级别(那是 ABAC/Policy 的领域),否则管理成本会很快失控。
5. 前端接入范式(最重要:怎么在代码里用)
这套模式的核心目标之一就是:让开发者在页面里用一种很一致的写法接入权限。
5.1 useCan:纯判断(适合提交兜底)
import { useCan } from '@/authz';
import { message } from 'antd';
const { allowed: canUpdate } = useCan('operations.customers.update');
const handleSubmit = async () => {
if (!canUpdate) {
message.warning('无权限');
return;
}
// ...真实提交逻辑
};
适用场景:
- 表单提交
- Modal 的 onOk
- 批量操作、导入导出等非按钮直达逻辑
5.2 AuthButton:按钮封装(推荐)
import { AuthButton } from '@/authz';
<AuthButton
type="primary"
perm="operations.*.create"
denyText="只读角色无权限"
onClick={() => setOpen(true)}
>
新增客户
</AuthButton>
关键点:
perm是权限点 code- 默认
mode="disable"(显示但不可点),也支持mode="hide"(直接隐藏) denyText用 tooltip 解释“为什么不可点”,减少用户困惑与客服成本
5.3 为什么“按钮禁用”还要“提交兜底”
企业级系统里必须默认“攻击者存在”,同时也要考虑工程上的意外调用:
- 用户可以通过 DevTools 手动触发函数
- 业务组件可能被复用,绕过按钮直接调用提交 handler
因此最佳实践是:
- UI 层(按钮)禁用/隐藏
- 提交 handler 内再做一次
useCan兜底
本项目在运营管理试点里就是这样做的。
6. 路由与菜单:为什么暂时仍用“角色数组”
你可能会问:既然按钮都用权限点了,为什么路由还在用 handle.permissions: ['ops','ops-viewer']?
原因是工程上的“渐进式落地”:
- 路由准入通常是粗粒度模块边界,角色数组已经足够表达
- 在后端不配合的阶段,先保证能快速落地、风险可控
- 后续若后端能下发 permissions,可以再统一迁移为“路由也用权限点”
现阶段建议的分工:
- 路由:角色数组(快速控模块)
- 按钮:权限点(细粒度控动作)
未来演进方向:
- 路由的
handle.permissions由 roles 迁移为 permission codes AuthRoute的校验逻辑从 “roles includes” 升级为 “can(permission)”
7. 运营管理试点:只读角色 ops-viewer 如何落地
企业常见需求:新增 ops-viewer,语义是“只读不可修改”。
在 RBAC 中这类角色非常适合用通配表达:
ops-viewer:operations.*.readops:operations.*
对应的体验结果(示意):
- 能进入运营管理模块(路由准入)
- 能看到客户列表与详情(读权限)
- “新增/编辑/充值写入/并发修改/定价写操作”等按钮变灰并提示“只读无权限”
这套策略的维护成本很低:后续你新增更多运营域页面,只要权限点命名遵守规范,operations.*.read 就能自动覆盖“读能力”。
8. 测试与调试:如何快速验证权限逻辑
8.1 最推荐:改 rbac.json 验证按钮权限
因为 rbac.json 是运行时加载的,你可以直接修改它来验证某个角色的行为:
- 让
ops-viewer临时可写:把operations.*.read改成operations.* - 精准开放某个动作:加一个
operations.customers.update
8.2 后端不方便配合时:强制覆盖 roles(仅本地调试)
如果你想不依赖后端返回的角色,直接用常量 roles 验证逻辑,可以在 /v1/oauth/me 的解析处覆盖:
src/api/auth.ts的getMe()中强制roles = ['ops-viewer']
或在路由守卫写入 userAtom 前覆盖:
src/router/AuthRoute.tsx内setUser({ ...me, roles: ['ops-viewer'] })
上述做法仅用于本地验证,不建议提交到主分支。
9. 常见坑与治理建议
9.1 “前端权限不是安全边界”
前端能做的只是:
- 降低误操作
- 提供一致体验
- 避免用户看到无意义入口
但无法阻止抓包直接调用写接口。企业级系统最终一定要后端鉴权。
9.2 权限点必须可治理
当权限点数量增长,务必建立:
- 命名规范
- 归属模块(domain)
- 权限点字典(可选:生成文档或配置清单)
- 新增权限点的评审流程(至少在 PR 里可见)
否则权限点会像“散落的字符串常量”一样失控。
9.3 “隐藏还是禁用”
企业常用策略:
- 高风险动作(删除/导出)倾向于隐藏(减少误触与信息泄露)
- 日常写动作(编辑/提交)倾向于禁用并提示原因(减少困惑)
本项目 AuthButton 同时支持 hide/disable,由业务选择。
10. 如何从前端-only 平滑演进到后端统一权限(路线图)
当后端允许改造时,推荐的升级路线是:
- 后端在
/me里下发permissions: string[] - 前端
useCan()的数据源从“roles->permissions 映射”切换为“后端下发 permissions” rbac.json转为兜底/降级策略(例如后端异常时保底只读)- 路由准入从 “roles includes” 迁移到 “permission codes”
- 进一步引入数据范围(ABAC / scope)处理“只能看自己部门客户”等条件权限
这套演进的关键是:前端接入点(useCan/AuthButton)保持不变,只替换“权限数据从哪来”。
11. 代码(Vue/React 可复制版本)
你会在后续看到本目录下新增:
react:一个最小可运行的 React 示例(含 rbac.json、useCan、AuthButton、路由守卫)vue:一个最小可运行的 Vue 示例(含 composable、指令/组件、路由守卫)
目标是:复制目录即可在任意项目里落地同样的模式。
更多推荐
所有评论(0)