成为全栈·React 管理后台篇·按钮级权限:能力映射、菜单过滤与自锁保护

成为全栈·React 管理后台篇·按钮级权限:能力映射、菜单过滤与自锁保护

前端权限的价值是让用户只看到能够完成的操作,并在高风险场景阻止明显误操作。真正的安全边界仍在后端。

前言

权限功能很容易从一行判断开始:user.role === 'admin'。用户管理菜单里写一次,路由里写一次,重置密码按钮再写一次。后来产品允许 editor 查看某个页面,三处条件只改了两处,于是菜单能点、路由却跳 403;或者直接输入地址能进,页面按钮又全部消失。

问题不是条件复杂,而是同一个能力被角色字符串重复描述。当前后台把角色到能力的映射收进纯函数,让菜单、路由和按钮消费同一判据,再为"管理员禁用自己"这种业务风险增加独立护栏。

从角色判断改成能力提问

角色是后端授予的身份,能力是页面正在询问的动作。组件真正关心的是"能否管理用户",而不是"是不是 admin":

ts 复制代码
export const canManageUsers = (actor: Actor) =>
  roleAtLeast(actor?.role, 'admin')

export const canManageArticles = (actor: Actor) =>
  roleAtLeast(actor?.role, 'editor')

export const canForceArticleStatus = (actor: Actor) =>
  roleAtLeast(actor?.role, 'admin')

今天这些能力恰好映射到角色等级。将来 editor 可以只读用户列表时,应该增加 canViewUsers,而不是让组件到处猜新的角色组合。

能力函数还在注释中对应具体契约端点。这样权限变更时,可以从后端 x-authz 追到前端入口,而不是凭页面名称推测。

菜单、路由和按钮各管一层体验

三处检查并不重复,它们分别解决不同问题:

  1. 菜单过滤:不展示必然无法使用的入口。
  2. 路由守卫:用户直接输入地址时给出明确去向。
  3. 按钮门控:同一页面内隐藏无权执行的具体动作。

菜单项直接挂能力函数:

ts 复制代码
{ to: '/articles', label: '文章管理', can: canManageArticles }

路由使用同一个判据:

tsx 复制代码
<RequireCan can={canManageUsers}>
  <UserListPage />
</RequireCan>

按钮则通过 Can 组件声明能力:

tsx 复制代码
<Can name="resetPassword">
  <Button>重置密码</Button>
</Can>

Can 内部只有一张能力映射表。业务组件没有角色字符串,也不需要知道等级算法。

归属权限不能只靠最低角色

有些端点允许 editor 操作所有资源,同时允许普通用户操作自己的资源。这个规则不是简单的 minRole

ts 复制代码
export const canOperateOwned = (
  actor: Actor,
  ownerId: number | undefined,
  min: UserRole,
) => {
  if (!actor) return false
  if (roleAtLeast(actor.role, min)) return true
  return ownerId !== undefined && actor.id === ownerId
}

组件可以传入 ownerId 与 min,让"本人或 editor"只在一处解释。需要警惕的是资源列表可能没有返回 ownerId;此时前端不能为了显示按钮猜测所有权,应选择不展示,或补充契约字段。

自锁保护不是 RBAC

admin 有权禁用用户,但如果编辑对象就是自己,允许选择"禁用"会把唯一管理员踢出后台。后端可能允许这个请求,因为从角色上看它完全合法;前端仍应阻止明显的高风险误操作。

ts 复制代码
const currentUser = useAuthStore((s) => s.user)
const isSelf = currentUser?.id === user?.id

// 编辑自己时禁用 inactive 选项

这叫业务护栏,不是新增权限。类似规则还包括不能删除正在使用的标签、不能在文章有未保存修改时直接下架。权限回答"有没有资格",护栏回答"此刻执行是否会造成明显问题"。

高风险动作还需要确认、清楚后果和进行中锁定。只把按钮染红,不足以阻止连续点击或误解操作结果。

前端隐藏按钮不构成安全

浏览器里的 JavaScript、路由和 store 都由用户控制。攻击者可以修改状态、调用控制台或直接构造 HTTP 请求。因此:

  • 后端必须验证 token、角色、资源所有权和当前状态;
  • 前端判据必须以契约为依据,但只负责体验;
  • 收到 403 时仍要正常展示错误,不能认为"按钮隐藏了就不会发生"。

前端权限测试的目标,是避免误导正常用户。例如 editor 不应看到用户管理入口,直接访问 /users 应得到 403 页面,admin 应能看到重置密码按钮。后端测试才证明越权请求确实被拒绝。

权限不足时,隐藏还是禁用

如果用户永远没有某项能力,隐藏通常更干净;如果动作暂时不可用,禁用并解释原因更合适。editor 永远不能管理站点设置,因此菜单隐藏;admin 正在保存用户时按钮暂时禁用;标签仍有文章引用时删除按钮禁用并提示原因。

把权限不足按钮全部禁用,会让界面布满无法操作的控件;把暂时不可用按钮全部隐藏,又会让布局跳动,用户不知道功能去哪了。两者要按原因区分。

能力粒度要跟业务动作一致

"管理文章"并不能覆盖所有文章动作。editor 可以查看后台文章、编辑和审核,但任意强改状态只允许 admin。因此代码同时存在 canManageArticlescanForceArticleStatus

若只设计一个 article:write,要么给 editor 暴露下架等越权按钮,要么让 admin 也失去专属动作。能力不必细到每个 DOM 元素,却应在权限规则或业务后果不同的地方拆开。

同理,canManageUserscanResetPassword 目前都要求 admin,但仍是两个能力。未来可能允许客服查看用户,却不允许重置密码;现在保留动作语义,届时不需要重写页面结构。

一张实用的能力表可以这样审阅:

能力 editor admin 主要入口
manageArticles 文章菜单、列表、编辑路由
moderateComments 评论审核
manageCategories / Tags 内容组织
manageUsers / resetPassword 用户管理
manageSiteSettings 站点设置
forceArticleStatus 已发布文章下架

它不是另建一份配置真相源,而是对 permission.ts 和契约端点的审阅视图。真正的映射仍保留在可测试的函数中。

会话中的角色发生变化怎么办

管理员可能在另一个窗口修改当前用户角色,或者账号在会话期间被禁用。前端内存里的 user 不会凭空知道变化。下一次受保护请求由后端返回 403 或账号禁用错误,请求层再刷新会话或强制退出。

因此能力函数只能基于"当前已知身份"改善界面,不能承诺权限实时有效。对高风险系统,可以增加短轮询、服务端推送或每次恢复时重新获取 /auth/me;无论如何,请求时的后端判断才是最终结论。

这也解释了为什么收到 403 后不能只在控制台打印错误。页面需要展示可理解的权限变化提示,并失效可能依赖用户角色的查询,避免用户不断点击一个已经失效的入口。

如何验证三层没有漂移

最小权限验收应使用 editor 和 admin 两个真实角色,逐项检查:菜单是否出现、直接地址是否放行、页面按钮是否正确、最终 API 是否允许。只做组件快照无法覆盖路由与服务端。

能力函数本身适合纯函数测试,例如空用户、member、editor、admin,以及本人资源与他人资源的组合。测试的意义是钉住契约映射,而不是证明隐藏按钮能够保证安全。

还有一种常见漂移来自菜单配置和路由配置分离:新增页面时只加了其中一处。代码审阅应把"入口、地址、能力"视为一组变更;浏览器验收则从菜单点击和直接地址两条路径分别进入。对于完全隐藏的 admin 页面,更要用 editor 手工输入地址验证守卫,而不是因为菜单里看不见就算通过。

无权限页面也要提供可行动的去向。未登录回登录页,角色不能进入后台去 no-access,单个能力不足去 403 并允许返回。把所有情况都重定向首页,会让用户误以为链接损坏,也让权限配置错误难以排查。

小结

当前后台用能力函数维护角色映射,让菜单、路由和按钮共用判据;所有权规则收进独立函数;自锁、引用占用等风险由业务护栏处理。三层体验可以互相补位,却都不替代后端授权。

各位看官,权限审阅时请不要只换账号看菜单。还要直接输入路由、检查页面动作,并实际向后端发送越权请求。入口一致、反馈清楚、服务端拒绝,才是一条完整权限链。

延伸阅读


如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」

🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html

📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer

相关推荐
FungLeo3 小时前
成为全栈·React 管理后台篇·评论审核工作流:把状态下拉改成可理解的动作
react·内容安全·状态机·交互设计·成为全栈·评论审核
SL-staff4 小时前
MQTT Topic权限越界排查指南:JVS-IOT中系统Topic与自定义Topic的鉴权机制与实操验证
mqtt·嵌入式开发·权限控制·物联网开发·协议分析·鉴权机制·jvs-iot
liangshanbo121516 小时前
React 状态持久化面试题——原理深挖版
react·状态持久化
liangshanbo121516 小时前
Redux 的中间件(Middleware)的工作机制
中间件·react·redux
FungLeo1 天前
成为全栈·React 管理后台篇·Markdown 编辑器:预览、暗色主题与连续图片粘贴
图片上传·react·响应式设计·markdown编辑器·异步编程·成为全栈
hey you~2 天前
企业400电话数据统计接口怎么开发,支持自定义报表?
权限控制·物化视图·自定义报表·异步导出·企业400电话·数据统计接口·压测优化
FungLeo2 天前
成为全栈·React 管理后台篇·表单页范式:校验、数据回填与未保存保护
react·表单设计·zod·react hook form·成为全栈·数据回填
名字还没想好☜2 天前
React 文件上传实战:预览 URL 回收、多文件逐个进度与拖拽放置区踩坑
前端·react·next.js
FungLeo2 天前
成为全栈·React 管理后台篇·列表页范式:让分页、筛选和返回位置进入 URL
react·前端架构·分页查询·react router·成为全栈·url状态