权限设计模式前端 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-viewerPermission Code权限点一个动作能力customer.update、recharge.writeRole-Permission Mapping角色拥有哪些权限点可配置这样你新增角色时只需要维护“角色拥有哪些权限点”而不是全项目搜改 if/else。2. 本项目的落地结构工程视角2.1 文件结构核心入口这套模式在本仓库的关键文件如下此为原项目目录非示例代码结构运行时配置文件public/rbac.json配置加载与兜底src/store/authz.ts权限匹配核心支持 * 通配src/authz/rbac.tsReact Hook 入口src/authz/useCan.ts按钮封装组件src/authz/AuthButton.tsx路由守卫同时负责拉取 /mesrc/router/AuthRoute.tsx路由表模块准入用 roles 数组src/router/index.tsx菜单过滤模块入口可见性用 roles 数组src/layouts/BasicLayout.tsx2.2 运行时数据流从登录到按钮权限一个典型的页面进入流程可以抽象成登录成功 - token 持久化 | 进入受保护路由AuthRoute | 1) 拉取 /v1/oauth/me 得到 user roles后端返回 2) 拉取 /rbac.json 得到 role-permissions静态配置 | 路由级handle.permissionsroles决定能否进入 按钮级useCan/AuthButtonpermission code决定可见/可点/可提交注意这套前端 RBAC 的实现不要求后端返回 permissions只要后端能返回 roles哪怕只是一个ops-viewer前端就能用本地映射推导出动作能力。3. 配置rbac.json 怎么写、怎么匹配3.1 配置文件格式role - permissionspublic/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 typeprimary permoperations.*.create denyText只读角色无权限 onClick{() setOpen(true)} 新增客户 /AuthButton关键点perm是权限点 code默认modedisable显示但不可点也支持modehide直接隐藏denyText用 tooltip 解释“为什么不可点”减少用户困惑与客服成本5.3 为什么“按钮禁用”还要“提交兜底”企业级系统里必须默认“攻击者存在”同时也要考虑工程上的意外调用用户可以通过 DevTools 手动触发函数业务组件可能被复用绕过按钮直接调用提交 handler因此最佳实践是UI 层按钮禁用/隐藏提交 handler 内再做一次useCan兜底本项目在运营管理试点里就是这样做的。6. 路由与菜单为什么暂时仍用“角色数组”你可能会问既然按钮都用权限点了为什么路由还在用handle.permissions: [ops,ops-viewer]原因是工程上的“渐进式落地”路由准入通常是粗粒度模块边界角色数组已经足够表达在后端不配合的阶段先保证能快速落地、风险可控后续若后端能下发 permissions可以再统一迁移为“路由也用权限点”现阶段建议的分工路由角色数组快速控模块按钮权限点细粒度控动作未来演进方向路由的handle.permissions由 roles 迁移为 permission codesAuthRoute的校验逻辑从 “roles includes” 升级为 “can(permission)”7. 运营管理试点只读角色 ops-viewer 如何落地企业常见需求新增ops-viewer语义是“只读不可修改”。在 RBAC 中这类角色非常适合用通配表达ops-vieweroperations.*.readopsoperations.*对应的体验结果示意能进入运营管理模块路由准入能看到客户列表与详情读权限“新增/编辑/充值写入/并发修改/定价写操作”等按钮变灰并提示“只读无权限”这套策略的维护成本很低后续你新增更多运营域页面只要权限点命名遵守规范operations.*.read就能自动覆盖“读能力”。8. 测试与调试如何快速验证权限逻辑8.1 最推荐改 rbac.json 验证按钮权限因为rbac.json是运行时加载的你可以直接修改它来验证某个角色的行为让ops-viewer临时可写把operations.*.read改成operations.*精准开放某个动作加一个operations.customers.update8.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、指令/组件、路由守卫目标是复制目录即可在任意项目里落地同样的模式。
权限设计模式【前端 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-viewerPermission Code权限点一个动作能力customer.update、recharge.writeRole-Permission Mapping角色拥有哪些权限点可配置这样你新增角色时只需要维护“角色拥有哪些权限点”而不是全项目搜改 if/else。2. 本项目的落地结构工程视角2.1 文件结构核心入口这套模式在本仓库的关键文件如下此为原项目目录非示例代码结构运行时配置文件public/rbac.json配置加载与兜底src/store/authz.ts权限匹配核心支持 * 通配src/authz/rbac.tsReact Hook 入口src/authz/useCan.ts按钮封装组件src/authz/AuthButton.tsx路由守卫同时负责拉取 /mesrc/router/AuthRoute.tsx路由表模块准入用 roles 数组src/router/index.tsx菜单过滤模块入口可见性用 roles 数组src/layouts/BasicLayout.tsx2.2 运行时数据流从登录到按钮权限一个典型的页面进入流程可以抽象成登录成功 - token 持久化 | 进入受保护路由AuthRoute | 1) 拉取 /v1/oauth/me 得到 user roles后端返回 2) 拉取 /rbac.json 得到 role-permissions静态配置 | 路由级handle.permissionsroles决定能否进入 按钮级useCan/AuthButtonpermission code决定可见/可点/可提交注意这套前端 RBAC 的实现不要求后端返回 permissions只要后端能返回 roles哪怕只是一个ops-viewer前端就能用本地映射推导出动作能力。3. 配置rbac.json 怎么写、怎么匹配3.1 配置文件格式role - permissionspublic/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 typeprimary permoperations.*.create denyText只读角色无权限 onClick{() setOpen(true)} 新增客户 /AuthButton关键点perm是权限点 code默认modedisable显示但不可点也支持modehide直接隐藏denyText用 tooltip 解释“为什么不可点”减少用户困惑与客服成本5.3 为什么“按钮禁用”还要“提交兜底”企业级系统里必须默认“攻击者存在”同时也要考虑工程上的意外调用用户可以通过 DevTools 手动触发函数业务组件可能被复用绕过按钮直接调用提交 handler因此最佳实践是UI 层按钮禁用/隐藏提交 handler 内再做一次useCan兜底本项目在运营管理试点里就是这样做的。6. 路由与菜单为什么暂时仍用“角色数组”你可能会问既然按钮都用权限点了为什么路由还在用handle.permissions: [ops,ops-viewer]原因是工程上的“渐进式落地”路由准入通常是粗粒度模块边界角色数组已经足够表达在后端不配合的阶段先保证能快速落地、风险可控后续若后端能下发 permissions可以再统一迁移为“路由也用权限点”现阶段建议的分工路由角色数组快速控模块按钮权限点细粒度控动作未来演进方向路由的handle.permissions由 roles 迁移为 permission codesAuthRoute的校验逻辑从 “roles includes” 升级为 “can(permission)”7. 运营管理试点只读角色 ops-viewer 如何落地企业常见需求新增ops-viewer语义是“只读不可修改”。在 RBAC 中这类角色非常适合用通配表达ops-vieweroperations.*.readopsoperations.*对应的体验结果示意能进入运营管理模块路由准入能看到客户列表与详情读权限“新增/编辑/充值写入/并发修改/定价写操作”等按钮变灰并提示“只读无权限”这套策略的维护成本很低后续你新增更多运营域页面只要权限点命名遵守规范operations.*.read就能自动覆盖“读能力”。8. 测试与调试如何快速验证权限逻辑8.1 最推荐改 rbac.json 验证按钮权限因为rbac.json是运行时加载的你可以直接修改它来验证某个角色的行为让ops-viewer临时可写把operations.*.read改成operations.*精准开放某个动作加一个operations.customers.update8.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、指令/组件、路由守卫目标是复制目录即可在任意项目里落地同样的模式。