refine useCan Hook 深度指南:基于 Access Control Provider 的权限校验、查询缓存与源码解析
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
useCan是 refine 核心包中面向 Access Control Provider 的权限校验 Hook。它以accessControlProvider.can作为 TanStack Query(react-query)useQuery的查询函数,将“权限是否允许”这一判定纳入统一的数据查询体系,从而获得缓存、去重与状态管理能力。读完本文,你将掌握useCan的完整参数、返回值与性能优化手段,并通过仓库源码理解其底层实现与边界行为,能在 refine 3.x 项目中独立实现细粒度的 RBAC / ABAC 权限控制。
本文主体对应仓库文档 useCan.md,所有源码依据均来自当前仓库packages/core与documentation/versioned_docs目录。
useCan 是什么:把「权限判定」变成一次数据查询
在 refine 的权限模型中,accessControlProvider只需实现一个异步方法can,用于回答“用户对某资源执行某动作是否被允许”。refine 刻意保持 API 无关性(agnostic),以便对接 RBAC、ABAC、ACL 等不同方案以及 Casbin、CASL、Cerbos 等库——can方法正是这些方案的统一入口,详见 accessControl-provider.md。
useCan的核心设计是:把can函数当作useQuery的查询函数。它接受can所需的一切参数(resource、action、params),并额外支持queryOptions用于配置useQuery,最终返回useQuery的查询结果。
从源码看,其签名与实现位于 packages/core/src/hooks/accessControl/useCan/index.ts:
export const useCan = ({ action, resource, params, queryOptions: hookQueryOptions, }: UseCanProps): UseQueryResult<CanReturnType> => { // 从 AccessControlContext 中取出 can 与全局 options const { can, options: globalOptions } = useContext(AccessControlContext); ... };它从AccessControlContext读取can函数,再经由useQuery执行,因此useCan天然具备 react-query 的全部能力:查询状态机(loading / error / success)、缓存、重试控制、并发去重等。
版本说明:本仓库 version-3.xx.xx 文档中的示例使用
@pankod/refine-core包名(refine 3.x 时代的命名),当前仓库较新版本中对应包为@refinedev/core,用法保持一致,按你所使用的版本选择导入来源即可。
前置准备:Access Control Provider 与 can 方法
useCan的输入输出严格对齐can的类型。在 packages/core/src/contexts/accessControl/types.ts 中可以确认三组核心类型:
export type CanResponse = { can: boolean; reason?: string; [key: string]: unknown; }; export type CanParams = { resource?: string; // 资源名,用于 API 数据交互 action: string; // 对资源的意图动作 params?: { resource?: IResourceItem & { children?: ITreeResource[] }; id?: BaseKey; [key: string]: any; }; }; export type CanReturnType = { can: boolean; reason?: string; }; export type CanFunction = ({ resource, action, params, }: CanParams) => Promise<CanReturnType>;同时,IAccessControlContext除了can之外还支持全局options(见 types.ts):
type AccessControlOptions = { buttons?: { enableAccessControl?: boolean; hideIfUnauthorized?: boolean; }; queryOptions?: MakeOptional< UseQueryOptions<CanReturnType>, "queryFn" | "queryKey" >; }; export interface IAccessControlContext { can?: CanFunction; options?: AccessControlOptions; }这意味着你既可以在单次调用时传queryOptions,也可以在 Provider 层配置全局的queryOptions(后文“源码深挖”会说明二者如何合并)。
基本用法
useCan的最基本用法如下(沿用原文档示例):
import { useCan } from "@pankod/refine-core"; const { data } = useCan({ resource: "resource-you-ask-for-access", action: "action-type-on-resource", params: { foo: "optional-params" }, });调用后,data即为can方法的返回结果:{ can: boolean; reason?: string }。例如判断“当前用户能否创建 post”,结合 Provider 定义与 Hook 调用的完整示例为:
<Refine accessControlProvider={{ can: async ({ resource, action }) => { if (resource === "post" && action === "create") { return Promise.resolve({ can: false, reason: "Unauthorized", }); } return Promise.resolve({ can: true }); }, }} // ... />; // inside your component const { data: canCreatePost } = useCan({ action: "create", resource: "post", }); console.log(canCreatePost); // { can: false, reason: "Unauthorized" }reason字段会随can一起返回,可用于向用户展示被拒原因;在 refine 内置按钮中,reason还会显示在按钮禁用态的工具提示(tooltip)中,参见 accessControl-provider.md。
Properties 详解
useCan接受四个属性,前三个原样透传给can,最后一个用于配置底层查询。
resource(必填)
传递给can函数的resource参数,表示要请求访问的资源名:
useCan({ resource: "resource-you-ask-for-access", });action(必填)
传递给can函数的action参数,表示在资源上执行的意图动作(如list、create、edit、show、delete等):
useCan({ action: "resource-you-ask-for-access", });原文档此处的示例字符串沿用了占位写法,实际使用时应填写动作类型而非资源名,例如
action: "edit"。
params
传递给can函数的params参数,通常用于携带记录级信息,例如id,或完整的资源对象以实现 ABAC:
useCan({ params: { foo: "optional-params" }, });queryOptions
透传给 TanStack QueryuseQuery的查询配置,典型用途是调整staleTime、cacheTime、enabled、queryKey、queryFn等:
useCan({ queryOptions: { staleTime: 5 * 60 * 1000, // 5 minutes }, });在 useCan/index.ts 中,UseCanProps定义为CanParams与queryOptions的交集,其中queryOptions允许自定义queryKey和queryFn:
export type UseCanProps = CanParams & { queryOptions?: Omit<UseQueryOptions<CanReturnType>, "queryKey"> & { queryKey?: UseQueryOptions<CanReturnType>["queryKey"]; }; };返回值
useCan的返回值就是useQuery的查询结果,类型为QueryObserverResult(数据部分为CanReturnType)。你可以直接使用 react-query 提供的全部字段,如data、isLoading、isError、isFetched、refetch等。
原文档给出的 API 对照如下:
| 属性 | 描述 |
|---|---|
CanReturnType | 查询结果数据类型,即{ can: boolean; reason?: string }(原文档同时提及HttpError类型的错误场景,实际以你使用的 refine 版本接口定义为准) |
| 描述 | 类型 |
|---|---|
TanStack QueryuseQuery的查询结果 | QueryObserverResult<{ data: CanReturnType; }>(源码中为UseQueryResult<CanReturnType>) |
值得注意的一个兜底行为:在 useCan/index.ts 中,如果当前上下文中不存在can函数(例如未配置accessControlProvider),useCan不会抛错,而是直接返回{ data: { can: true } },即默认放行:
return typeof can === "undefined" ? ({ data: { can: true } } as typeof queryResponse) : queryResponse;性能优化:善用 staleTime 与 cacheTime
随着应用内权限检查点的增加,尤其当权限判定依赖远程端点时,性能可能明显下降。由于 refine 基于 TanStack Query,缓存权限检查结果能带来巨大收益,最简单的方式就是配置staleTime与cacheTime:
import { useCan } from "@pankod/refine-core"; // inside your component 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 } });staleTime:数据在多少毫秒内被视为“新鲜”,期间命中缓存不会重新请求;cacheTime:不再被使用的查询结果在缓存中保留的时长。
关于默认值,accessControl-provider.md 明确说明:refine 自带的权限检查点默认使用 5 分钟cacheTime、0staleTime。这意味着默认情况下每次挂载都可能重新请求(因为staleTime: 0),而缓存 5 分钟。如果你的权限变更不频繁,调大staleTime可以有效减少重复请求。
源码深挖:useCan 的实现细节
1. queryKey 的自动生成
在 useCan/index.ts 中,查询键由useKeys()生成,将resource、action、params(含enabled状态)全部纳入键结构:
const queryResponse = useQuery<CanReturnType>({ queryKey: keys() .access() .resource(resource) .action(action) .params({ params: { ...paramsRest, resource: sanitizedResource }, enabled: mergedQueryOptions?.enabled, }) .get(), queryFn: () => can?.({ action, resource, params: { ...paramsRest, resource: sanitizedResource }, }) ?? Promise.resolve({ can: true }), enabled: typeof can !== "undefined", ...mergedQueryOptions, meta: { ...mergedQueryOptions?.meta, ...getXRay("useCan", resource, [ "useButtonCanAccess", "useNavigationButton", ]), }, retry: false, });几个关键点:
enabled: typeof can !== "undefined":未配置权限 Provider 时查询不会执行,配合上面的兜底返回{ can: true };retry: false:权限请求默认不自动重试(与一般数据请求的默认策略不同),避免权限接口失败时产生无意义的重复请求;meta注入getXRay信息:为 refine devtools 提供 Hook 调用链路追踪(useButtonCanAccess、useNavigationButton等);queryFn兜底:can为undefined时返回Promise.resolve({ can: true }),保证类型安全与行为一致。
2. 全局 queryOptions 与局部 queryOptions 合并
useCan支持在 Provider 层配置全局queryOptions(见AccessControlOptions.queryOptions)。源码中的合并逻辑是浅合并:全局配置作为基础,单次调用传入的配置覆盖之:
const { queryOptions: globalQueryOptions } = globalOptions || {}; const mergedQueryOptions = { ...globalQueryOptions, ...hookQueryOptions, };在 index.spec.tsx 中有对应测试:当 Provider 配置options: { queryOptions: { enabled: false } }时,can不会被调用,验证了全局配置确实生效。
3. sanitizeResource:防止不可序列化的 icon 破坏 queryKey
由于 react-query 会对 queryKey 做字符串化处理,如果params.resource中带有icon等包含ReactNode的属性,可能引发循环依赖序列化错误。为此useCan在构建 queryKey 与调用can之前,会通过sanitizeResource清洗资源对象,见 sanitize-resource/index.ts:
export const sanitizeResource = (resource) => { const { list, edit, create, show, clone, children, meta, icon, ...restResource } = resource; const { icon: _metaIcon, ...restMeta } = meta ?? {}; return { ...restResource, ...(meta ? { meta: restMeta } : {}), }; };该函数会剔除资源对象中不可序列化的list / edit / create / show / clone组件引用、icon(含meta.icon)等属性,只保留纯数据字段。相关行为在 index.spec.tsx 有专门测试:传入带meta.icon的资源对象后,can实际收到的params.resource中icon已被移除。
4. useCanWithoutCache:跳过缓存的直通变体
除useCan外,refine 还提供了useCanWithoutCache(useCanWithoutCache.ts)。它不做任何查询缓存,直接从AccessControlContext取出can函数并返回,同时应用相同的sanitizeResource清洗逻辑:
export const useCanWithoutCache = (): IAccessControlContext => { const { can: canFromContext } = React.useContext(AccessControlContext); // 包装一层:对 params.resource 做 sanitize 后调用原始 can ... return { can }; };适用场景:当某次权限判定必须“实时”执行、不能命中缓存时(例如刚完成角色变更,需要立即刷新判定结果),使用该变体可获得不带缓存的can函数引用。
5. 组件与框架层的联动
useCan是 refine 权限体系的底层 Hook,上层还有多个消费方:
<CanAccess />组件:内部直接调用useCan(见 canAccess/index.tsx),根据data.can决定渲染children还是fallback,并支持onUnauthorized回调;- 路由层:
@pankod/refine-nextjs-router、@pankod/refine-react-router、@pankod/refine-react-location会对 CRUD 页面[resource]/[action]做权限检查,失败时展示catchAll或标准错误页; - Sider 菜单:不可访问的资源不会出现在侧边栏;
- 各类按钮(List / Create / Clone / Edit / Delete / Show):权限判定失败时按钮被禁用并展示
reason。
各检查点的具体{ resource, action, params }参数约定,可参阅 accessControl-provider.md 的 “List of Default Access Control Points” 章节。
测试用例验证
仓库为useCan提供了完整的单元测试,见 packages/core/src/hooks/accessControl/useCan/index.spec.tsx,覆盖了以下行为,可作为你理解和使用该 Hook 的参考清单:
| 测试场景 | 验证结论 |
|---|---|
can返回{ can: true, reason: "Access granted" } | 正确透传data.can与data.reason |
can返回{ can: false, reason: "Access Denied" } | 拒绝场景数据透传正确 |
Provider 未提供can(undefined) | 返回兜底{ data: { can: true } },不抛错 |
params.resource携带meta.icon | sanitizeResource生效,can收到清洗后的资源对象 |
queryOptions.enabled: false | can不会被调用,查询被禁用 |
自定义queryOptions.queryKey | 覆盖默认 queryKey,缓存按自定义键存储 |
自定义queryOptions.queryFn | 覆盖默认查询函数,can不再被调用 |
Provider 全局options.queryOptions | 全局配置生效,可统一禁用查询 |
实践建议
- 优先使用
useCan做组件内权限判断:它能复用 react-query 缓存,多个组件检查相同resource + action时只会发起一次请求; - 合理设置
staleTime:权限策略变化不频繁的应用可设置较大的staleTime(如 5 分钟)显著降低远程权限接口压力;需要即时响应的场景则保持默认或设为 0; - 对 ABAC 场景善用
params.resource:can方法可通过params.resource拿到完整资源对象(含你在<Refine />中定义的meta等字段),实现基于属性的授权; - 注意 queryKey 的序列化限制:不要在
params.resource中携带icon等不可序列化内容,useCan会自动清洗,但了解这一点有助于排查异常 queryKey; - 实时判定用
useCanWithoutCache:需要绕过缓存、强制走最新权限逻辑时使用。
更多权限控制实战可参考仓库中的 examples/access-control-casbin 示例目录。至此,你已经掌握了useCan从参数、返回值到性能调优与源码原理的完整链路,可以在自己的 refine 项目中放心使用它构建细粒度、可缓存的权限控制。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考