Refine v5 接入 Cerbos:基于 accessControlProvider 的 RBAC 访问控制实战
2026/9/12 7:02:33 网站建设 项目流程

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-cerbos

create-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"roleslocalStorage读取的roleadmineditor),policyVersion"default"attributes用于承载用户级属性——这是 ABAC(基于属性的访问控制)的接入点;
  • resource(资源)kind直接取自 Refine 的resource参数(即 posts / users / categories),idparams?.id,新建场景下回退为"new"attributes透传 Refine 的params,可用于携带资源字段信息;
  • actions(动作):将 Refine 的action(list / create / edit / show / delete / field 等)原样交给 Cerbos 判定;
  • 返回结构can方法返回Promise<CanResponse>,其中canresult.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默认truehideIfUnauthorized默认false(即默认"禁用"而非"隐藏"无权按钮)。单个按钮可独立覆盖,未配置时回退到全局值;
  • options.queryOptions用于全局配置can查询的 react-query 选项。

注意:仅把accessControlProvider传给<Refine />并不会自动强制访问控制,还需要用<CanAccess />包裹受保护的路由或组件(详见 useCan 文档 与 CanAccess 文档)。

另外,can方法中可以通过params?.resource拿到完整的资源配置对象,包括自定义的meta字段,从而在资源声明的元数据层面做 ABAC 判定,例如"当资源的 meta 中某个标志为 true 时禁止编辑"。

字段级鉴权:用 useCan 控制表格列

示例中最能体现"细粒度"的部分在 posts/list.tsx:它用useCanposts资源上自定义的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(未提供时不渲染任何内容)。它的完整签名支持resourceactionparamsfallbackqueryOptions

<CanAccess resource="posts" action="edit" params={{ id: 1 }} fallback={<CustomFallback />} queryOptions={{ cacheTime: 25000 }} > <YourComponent /> </CanAccess>

与此同时,Refine 的默认访问控制点会自动生效:

  • Sider 菜单:菜单项以{ resource, action: "list" }发起鉴权,无权的资源不会出现在侧边栏中——这就是切换角色后菜单随之变化的原因;
  • 按钮组件ListButtonCreateButtonCloneButtonEditButtonDeleteButtonShowButton在渲染时会按对应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"重新读取角色,进而影响accessControlProviderprincipal.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.md
  • useCan钩子参考: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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询