Refine v5 接入 Cerbos:基于 accessControlProvider 的 RBAC 访问控制实战
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
访问控制是后台管理系统中最复杂也最容易被忽视的环节之一,涉及 RBAC、ABAC、ACL 等多种模型与大量细粒度授权场景。本篇文章基于 Refine 官方示例access-control-cerbos,完整讲解如何将 Cerbos(一个以策略为核心的授权服务)通过 Refine 的accessControlProvider接入应用,实现角色切换、字段级鉴权、路由级鉴权与按钮自动禁用,读完即可在真实 React 管理后台中复刻整套方案。
示例概览:Refine + Cerbos 的分工
Refine 提供了一套与具体授权方案解耦的 API——accessControlProvider。它不关心你背后用的是 RBAC 还是 ABAC,也不关心是本地规则还是远程授权服务,只要求你暴露一个异步的can方法,用于回答"某资源上的某动作是否被允许"。Cerbos 则是这套 API 的"策略决策引擎":你在 Cerbos 端编写策略(Policy),由 Cerbos 的 PDP(Policy Decision Point)根据请求中的主体(principal)、资源(resource)与动作(actions)给出允许/拒绝结论。
在 examples/access-control-cerbos 示例中,二者的分工非常清晰:
- Refine 负责渲染页面、表格、按钮,并在关键位置触发鉴权请求;
- Cerbos 负责根据当前用户的角色(admin / editor)与目标资源(posts / users / categories)做出授权判断;
- 角色通过顶部 Header 的 Radio 切换并写入
localStorage,刷新后生效,方便直观演示不同角色看到的不同界面。
示例的技术栈为 React 19 + Ant Design 5 + React Router 7,依赖了@cerbos/http、@refinedev/core、@refinedev/antd、@refinedev/simple-rest等(见 package.json),数据源使用https://api.fake-rest.refine.dev。
快速运行示例
在本地启动该示例只需一条命令:
npm create refine-app@latest -- --example access-control-cerboscreate-refine-app会自动拉取仓库中的 examples/access-control-cerbos 目录并安装依赖。之后在项目内执行:
npm run dev即可在浏览器中打开应用。页面顶部会出现 Admin / Editor 两个角色按钮,切换角色后应用会重新加载,界面上的菜单项、操作按钮与字段随之发生变化。
核心实现:将 Cerbos 封装为 accessControlProvider
整个示例的关键代码集中在 src/App.tsx。第一步是初始化 Cerbos 的 HTTP 客户端,指向 Cerbos 的托管 PDP:
import { HTTP as Cerbos } from "@cerbos/http"; // The Cerbos PDP instance const cerbos = new Cerbos("https://demo-pdp.cerbos.cloud", { playgroundInstance: "WS961950bd85QNYlAvTmJYubP0bqF7e3", // The playground instance ID to test });这里使用的是 Cerbos 托管的演示 PDP 与 Playground 实例(示例中的策略定义在 Cerbos Playground 中维护)。代码注释明确指出:生产环境建议在自己的应用旁边以容器方式部署 PDP 实例,而不是依赖演示端点。
第二步是在<Refine />组件上挂载accessControlProvider,将 Cerbos 的checkResource调用包装成 Refine 期望的can方法:
<Refine routerProvider={routerProvider} dataProvider={dataProvider(API_URL)} accessControlProvider={{ can: async ({ action, params, resource }) => { const result = await cerbos.checkResource({ principal: { id: "demoUser", // Fake a user ID roles: [role], policyVersion: "default", // this is where user attributes can be passed attributes: {}, }, resource: { kind: resource ?? "", policyVersion: "default", id: `${params?.id}` || "new", attributes: params, }, // the list of actions on the resource to check authorization for actions: [action], }); return Promise.resolve({ can: result.isAllowed(action) || false, }); }, }} resources={[ { name: "posts", list: "/posts", show: "/posts/show/:id", create: "/posts/create", edit: "/posts/edit/:id", meta: { canDelete: true } }, { name: "users", list: "/users", show: "/users/show/:id", create: "/users/create", edit: "/users/edit/:id" }, { name: "categories", list: "/categories", show: "/categories/show/:id", create: "/categories/create", edit: "/categories/edit/:id" }, ]} ... >这段代码值得逐点拆解,它体现了 Refine 的CanParams与 Cerbos 请求模型之间的映射:
- principal(主体):
id为模拟用户"demoUser",roles从localStorage读取的role(admin或editor),policyVersion为"default",attributes用于承载用户级属性——这是 ABAC(基于属性的访问控制)的接入点; - resource(资源):
kind直接取自 Refine 的resource参数(即 posts / users / categories),id取params?.id,新建场景下回退为"new",attributes透传 Refine 的params,可用于携带资源字段信息; - actions(动作):将 Refine 的
action(list / create / edit / show / delete / field 等)原样交给 Cerbos 判定; - 返回结构:
can方法返回Promise<CanResponse>,其中can取result.isAllowed(action)的结果。
通过这种包装,Refine 内部所有的鉴权点——Sider 菜单、各类按钮、useCan、<CanAccess />——都会自动走这条链路。
理解 accessControlProvider 接口
can方法背后的接口定义可以从 Access Control Provider 文档 中看到完整形态:
export interface IAccessControlContext { can?: ({ resource, action, params }: CanParams) => Promise<CanResponse>; options?: { buttons?: { enableAccessControl?: boolean; hideIfUnauthorized?: boolean; }; queryOptions?: UseQueryOptions<CanReturnType>; }; } const accessControlProvider: IAccessControlContext = { can: async ({ resource, action, params }: CanParams): Promise<CanResponse> => { return { can: true }; }, options: { buttons: { enableAccessControl: true, hideIfUnauthorized: false, }, queryOptions: { // ... default global query options }, }, };关键约定如下:
can至少接收{ resource, action, params },resource是你在<Refine />的resources中声明的资源对象(注意在 v5 中它是完整的资源配置对象而非字符串),params中通常携带id等记录级信息;- 返回值
CanResponse包含can: boolean,可选reason: string——当按钮因无权而被禁用时,reason会显示在按钮的 tooltip 中; options.buttons是全局按钮行为配置:enableAccessControl默认true,hideIfUnauthorized默认false(即默认"禁用"而非"隐藏"无权按钮)。单个按钮可独立覆盖,未配置时回退到全局值;options.queryOptions用于全局配置can查询的 react-query 选项。
注意:仅把
accessControlProvider传给<Refine />并不会自动强制访问控制,还需要用<CanAccess />包裹受保护的路由或组件(详见 useCan 文档 与 CanAccess 文档)。
另外,can方法中可以通过params?.resource拿到完整的资源配置对象,包括自定义的meta字段,从而在资源声明的元数据层面做 ABAC 判定,例如"当资源的 meta 中某个标志为 true 时禁止编辑"。
字段级鉴权:用 useCan 控制表格列
示例中最能体现"细粒度"的部分在 posts/list.tsx:它用useCan对posts资源上自定义的field动作做鉴权,决定是否渲染 "Hit" 列:
const { data: canAccess } = useCan({ resource: "posts", action: "field", params: { field: "hit" }, });然后在表格列定义中根据结果条件渲染:
{canAccess?.can && ( <Table.Column dataIndex="hit" title="Hit" render={(value: number) => ( <NumberField value={value} options={{ notation: "compact" }} /> )} /> )}useCan本质上把can方法作为 react-query 的查询函数封装(签名可见 useCan 文档),因此它支持queryOptions。这意味着你可以针对频繁调用的鉴权点做缓存配置:
const { data } = useCan({ resource: "resource-you-ask-for-access", action: "action-type-on-resource", params: { foo: "optional-params" }, queryOptions: { staleTime: 5 * 60 * 1000, // 5 minutes // ... other query options }, });默认情况下,Refine 自身的访问控制点使用5 分钟 cacheTime、0 分钟 staleTime(见 Access Control Provider 文档 的 Performance 一节)。如果鉴权涉及远程端点(本示例正是如此),缓存能显著减少对 PDP 的请求量。
路由级鉴权:用 CanAccess 保护整块区域
在 App.tsx 中,示例在布局层使用<CanAccess />包裹所有子路由的Outlet:
<ThemedLayout Header={() => <Header role={role} />}> <CanAccess> <Outlet /> </CanAccess> </ThemedLayout><CanAccess />内部使用useCan做鉴权,鉴权通过则渲染 children,否则渲染fallback(未提供时不渲染任何内容)。它的完整签名支持resource、action、params、fallback与queryOptions:
<CanAccess resource="posts" action="edit" params={{ id: 1 }} fallback={<CustomFallback />} queryOptions={{ cacheTime: 25000 }} > <YourComponent /> </CanAccess>与此同时,Refine 的默认访问控制点会自动生效:
- Sider 菜单:菜单项以
{ resource, action: "list" }发起鉴权,无权的资源不会出现在侧边栏中——这就是切换角色后菜单随之变化的原因; - 按钮组件:
ListButton、CreateButton、CloneButton、EditButton、DeleteButton、ShowButton在渲染时会按对应action自动发起鉴权(例如EditButton对应{ resource: "posts", action: "edit", params: { id: 1 } }),返回{ can: false }时按钮被禁用,hideIfUnauthorized: true时按钮被隐藏。
从源码结构看,Cerbos 的职责被完全收敛在accessControlProvider内部,页面组件无需感知 Cerbos 的存在——这正是 Refine 这套抽象的价值:未来把 Cerbos 换成 Casbin、CASL 或自研授权服务,页面层代码几乎不用改动。
角色切换:Header 与 localStorage
components/header.tsx 实现了示例的交互入口——一个 Ant Design 的Radio.Group,提供 Admin 与 Editor 两个角色:
<Radio.Group value={role} onChange={(event) => { localStorage.setItem("role", event.target.value); location.reload(); }} > <Radio.Button value="admin">Admin</Radio.Button> <Radio.Button value="editor">Editor</Radio.Button> </Radio.Group>切换角色后写入localStorage并刷新页面,App.tsx通过localStorage.getItem("role") ?? "admin"重新读取角色,进而影响accessControlProvider中principal.roles的值。role通过 props 传给 Header 用于高亮当前选中项。
在真实项目中,角色通常来自登录态(如 JWT、用户 Profile),而非localStorage;示例用localStorage是为了在无后端认证的情况下方便演示。接入时只需把principal.id换成真实用户 ID、roles换成服务端下发的角色列表,并考虑在登录/登出时刷新 Cerbos 客户端或鉴权查询缓存。
相关文件索引
- 示例入口与
accessControlProvider完整实现:examples/access-control-cerbos/src/App.tsx - 角色切换 Header:examples/access-control-cerbos/src/components/header.tsx
- 字段级鉴权(
useCan+ 条件渲染列):examples/access-control-cerbos/src/pages/posts/list.tsx - 其余页面(posts 增删改查、users、categories):examples/access-control-cerbos/src/pages/
- 依赖与脚本:examples/access-control-cerbos/package.json
- 本地运行说明:examples/access-control-cerbos/README.md
accessControlProvider接口与默认访问控制点:documentation/docs/authorization/access-control-provider/index.mduseCan钩子参考:documentation/docs/authorization/hooks/use-can/index.md<CanAccess />组件参考:documentation/docs/authorization/components/can-access/index.md
小结
通过access-control-cerbos示例可以总结出一条清晰的接入路径:初始化 Cerbos 客户端 → 在accessControlProvider.can中把CanParams映射为checkResource请求 → 在需要保护的路由外包<CanAccess />→ 用useCan实现字段级等自定义鉴权 → 借助options.buttons统一控制按钮的禁用/隐藏行为。由于 Refine 已内置 Sider、按钮等默认访问控制点,这套方案可以用极少的样板代码把 Cerbos 的策略能力铺满整个后台应用,同时保持页面代码与授权引擎的解耦。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考