---
description: "登录解决「你是谁」，这个形状解决「你能干什么」：一张 角色 × 权限矩阵 当唯一事实源，UI 端用 can() 禁用按钮、RoleGate 隐藏整块面板，服务端用 requireRole / requirePermission 把真正的门守住。不同用户该看到/做到不同的事，就用它"
---

# 权限门控（RBAC）

登录解决「你是谁」，这个形状解决「你能干什么」：一张 **角色 × 权限矩阵** 当唯一事实源，UI 端用 `can()` 禁用按钮、`RoleGate` 隐藏整块面板，服务端用 `requireRole / requirePermission` 把真正的门守住。不同用户该看到/做到不同的事，就用它。

<iframe src="/skills/dashboard/assets/demo-rbac.html"
        style="width:100%;height:820px;border:1px solid #8884;border-radius:10px"
        loading="lazy" title="形状 demo：权限门控"></iframe>

试这几下：

- 右上角切**访客 / 编辑 / 管理员** —— 工具栏按钮、行内操作、整个「危险操作」面板跟着底部那张矩阵变
- 访客/编辑看不到危险面板的位置，显示的是 `RoleGate` 的 **fallback** 占位
- 每个按钮问的都是同一个 `can(role, perm)` —— 页面里**没有一处** `if (role === "admin")`

## 规矩

- **矩阵是唯一事实源**：所有门控（禁用、隐藏、服务端守卫）都查 `ROLE_PERMISSIONS`，绝不散落角色字面量判断 —— 加角色/改权限只动一张表
- **UI 门控是体验，不是安全**：真正的边界在服务端 —— server fn 里 `requireUser()` 之后 `requirePermission(role, perm)`，不满足抛 `FORBIDDEN`（fail closed）。demo 里「服务端拒绝」的 toast 演示的就是这层语义
- `rbac.ts` 保持**客户端安全**（纯函数、不 import 数据库/认证），`can/hasRole` 才能在浏览器里跑
- 真实版把 `role` 存进用户表（默认 `viewer`），从路由上下文透出

> 蓝本：`add-rbac.md`[^add-rbac]（open-dashboard @ `aa9815f`，MIT，Invariants 已消化进上面「规矩」）

??? abstract "demo 源码：`assets/demo-rbac.html`（自包含、未压缩）"

    ```html
    --8<-- "skills/dashboard/assets/demo-rbac.html"
    ```

[^add-rbac]: [`add-rbac.md`](https://github.com/ahpxex/open-dashboard/blob/aa9815f426e9520841c4aa15a341bfd9f866091f/.claude/skills/add-component/references/add-rbac.md)，2026-07-28 读。
