Refine v5 审计日志(Audit Logs)完整指南:从 AuditLogProvider 到 useLog / useLogList
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本篇指南围绕 Refine v5 的 Audit Logs 体系展开,讲解如何通过auditLogProvider在数据变更时自动记录审计事件、如何用useLog与useLogList手动写入与查询日志,并深入源码剖析create/update/get三个方法背后的调用链与事件参数结构。读完本文,你将能够在自己的 Refine 应用中搭建一套可追溯、可审计、面向合规要求的操作日志机制。
为什么需要审计日志
审计日志(Audit Logs)是 Web 应用中非常有用的工具,它为用户操作与系统变更提供了一条可靠、可检索的记录。记录并存储这些日志能够保证系统行为的透明性与可问责性(accountability),这对**安全、合规(compliance)以及问题排查(debugging)**都至关重要。
在 Refine 中,审计日志的核心价值在于:CRUD 操作无需手动埋点即可自动记录。当你在<Refine>上提供auditLogProvider后,由useCreate、useUpdate、useDelete等数据 hook 发起的每一次成功变更,都会自动生成一条审计事件,并附带来自useGetIdentityhook 的当前用户信息。
Audit Log Provider 是什么
Refine 通过 Audit Log Provider 来集中、统一地获取与写入应用中的审计日志。它本质上是一个普通对象,包含三个方法:
create:向审计日志写入一条事件get:返回一组审计事件(列表查询)update:更新一条审计事件
其 TypeScript 接口定义在 packages/core/src/contexts/auditLog/types.ts 中,核心类型如下:
export type LogParams = { resource: string; action: string; data?: any; author?: { name?: string; [key: string]: any; }; previousData?: any; meta: Record<number | string, any>; }; export type IAuditLogContext = { create?: (params: LogParams) => Promise<any>; get?: (params: { resource: string; action?: string; meta?: Record<number | string, any>; author?: Record<number | string, any>; }) => Promise<any>; update?: (params: { id: BaseKey; name: string; [key: string]: any; }) => Promise<any>; }; export type AuditLogProvider = Required<IAuditLogContext>;可以看到AuditLogProvider就是IAuditLogContext的必选版本,create、get、update三者缺一不可。
在运行时,这些方法通过AuditLogContext注入到整个应用中,其实现见 packages/core/src/contexts/auditLog/index.tsx:AuditLogContextProvider接收create、get、update三个 prop,并将其放入 React Context。所有内置 hook(useLog、useLogList以及各数据 mutation hook)都通过useContext(AuditLogContext)拿到这三个方法。
一个最小可用的 Provider 示例
下面是一个完整的auditLogProvider示例(对应 documentation/docs/audit-logs/audit-log-provider/index.md 中的实战写法):
import { AuditLogProvider } from "@refinedev/core"; export const auditLogProvider: AuditLogProvider = { get: async (params) => { const { resource, meta, action, author } = params; const response = await fetch( `https://example.com/api/audit-logs/${resource}/${meta.id}`, { method: "GET", }, ); const data = await response.json(); return data; }, // 理想情况下,审计日志应该在服务端创建。 // 因为它可以被用户篡改,客户端创建的结果并不是可靠的真相来源。 create: async (params) => { const { resource, meta, action, author, data, previousData } = params; console.log(resource); // "products", "posts", 等 console.log(meta); // { id: "1" }, { id: "2" }, 等 console.log(action); // "create", "update", "delete" // author 对象是 `useGetIdentity` hook 的返回值 console.log(author); // { id: "1", name: "John Doe" } console.log(data); // { name: "Product 1", price: 100 } console.log(previousData); // { name: "Product 1", price: 50 } await fetch("https://example.com/api/audit-logs", { method: "POST", body: JSON.stringify(params), }); return { success: true }; }, update: async (params) => { const { id, name, ...rest } = params; console.log(id); // "1" console.log(name); // "Created Product 1" console.log(rest); // { foo: "bar" } await fetch(`https://example.com/api/audit-logs/${id}`, { method: "PATCH", body: JSON.stringify(params), }); return { success: true }; }, };注册到<Refine>
将 Provider 传给<Refine>组件的auditLogProviderprop 即可启用全局审计能力:
import { Refine } from "@refinedev/core"; import { auditLogProvider } from "./audit-log-provider"; export const App = () => { return <Refine auditLogProvider={auditLogProvider}>{/* ... */}</Refine>; };逐方法实现:get / create / update
get:查询审计事件列表
get方法用于获取一组审计日志事件。例如,通过useLogListhook 按某个记录 id 列出该资源的所有活动时,会向get发送如下事件:
{ "resource": "posts", "meta": { "id": "1" } }get收到的参数结构为{ resource, action?, meta?, author? },你的实现可以基于这些字段向服务端发起查询(如上面的 fetch 示例)。
create:写入审计事件
create方法在以下两种时机被触发:
- 一次成功的 mutation 之后(由数据 hook 自动触发);
- 手动调用
useLog的log方法时。
传入的参数表示即将创建的新记录的值。根据 mutation 类型不同,Refine 会为create组装不同的参数,规则如下(见 documentation/docs/audit-logs/audit-log-provider/index.md):
previousData来自 react-query 缓存,若能找到则返回旧值,否则为undefined;- 在 create 类 mutation 中,如果请求响应包含
id字段,该id会被加入到meta对象; - 如果 auth provider 中定义了
getUserIdentity,事件中会追加author对象,其值为getUserIdentity的返回值。
各 mutation 类型的create事件参数如下:
create(useCreate)
{ "action": "create", "resource": "posts", "data": { "title": "Hello World", "content": "Hello World" }, "meta": { "dataProviderName": "simple-rest", // 如果请求响应有 `id` 字段,会被加入 `meta` "id": 1 } }update(useUpdate)
{ "action": "update", "resource": "posts", "data": { "title": "New Hello World", "content": "New Hello World" }, "previousData": { "title": "Hello World", "content": "Hello World" }, "meta": { "dataProviderName": "simple-rest", "id": 1 } }delete(useDelete)
{ "action": "delete", "resource": "posts", "meta": { "dataProviderName": "simple-rest", "id": 1 } }createMany(useCreateMany)
{ "action": "createMany", "resource": "posts", "data": [ { "title": "Hello World 1" }, { "title": "Hello World 2" } ], "meta": { "dataProviderName": "simple-rest", // 如果请求响应有 `id` 字段,会被加入 `meta` "ids": [1, 2] } }updateMany(useUpdateMany)
{ "action": "updateMany", "resource": "posts", "data": { "status": "published" }, "previousData": [ { "status": "draft" }, { "status": "archived" } ], "meta": { "dataProviderName": "simple-rest", "ids": [1, 2] } }deleteMany(useDeleteMany)
{ "action": "deleteMany", "resource": "posts", "meta": { "dataProviderName": "simple-rest", "id": [1, 2] } }在 Provider 的create中,你可以解构这些字段并发送到服务端:
export const auditLogProvider: AuditLogProvider = { create: (params) => { const { resource, meta, action, author, data, previousData } = params; console.log(resource); // "products", "posts", 等 console.log(meta); // { id: "1" }, { id: "2" }, 等 console.log(action); // "create", "update", "delete" // author 对象是 `useGetIdentity` hook 的返回值 console.log(author); // { id: "1", name: "John Doe" } console.log(data); // { name: "Product 1", price: 100 } console.log(previousData); // { name: "Product 1", price: 50 } await fetch("https://example.com/api/audit-logs", { method: "POST", body: JSON.stringify(params), }); return { success: true }; }, };安全提醒:由于客户端数据可被用户篡改,官方建议将审计日志的实际落库放在服务端完成。客户端的
create只是"上报事件",不应被视为可靠的数据真相来源(source of truth)。
update:更新审计事件
update方法用于更新一条既有的审计事件。例如,通过useLog的rename方法会给update发送如下参数:
{ "id": "1", "name": "event name" }对应实现:
export const auditLogProvider: AuditLogProvider = { update: async (params) => { const { id, name, ...rest } = params; console.log(id); // "1" console.log(name); // "Created Product 1" console.log(rest); // { foo: "bar" } await fetch(`https://example.com/api/audit-logs/${id}`, { method: "PATCH", body: JSON.stringify(params), }); return { success: true }; }, };自动审计的底层原理:数据 hook 如何在成功后写日志
Refine 的自动审计并非黑魔法,而是内建于各个数据 mutation hook 的成功回调中。以useCreate为例,在 packages/core/src/hooks/data/useCreate.ts 中可以看到:当 mutation 成功后,Refine 会先执行通知(notification)与缓存失效(invalidate),随后调用:
const { fields: _fields, operation: _operation, variables: _variables, ...rest } = combinedMeta || {}; log?.mutate({ action: "create", resource: resource.name, data: values, meta: { ...rest, dataProviderName, id: data?.data?.id ?? undefined, }, });这段代码印证了文档中的两个细节:meta中会带上dataProviderName,并且当响应包含id时会把id塞进meta。log?.mutate(...)中log正是useLog返回的 mutation(通过内部 hook 组合注入),因此只要auditLogProvider存在,create方法就会被自动调用。useUpdate、useDelete、useCreateMany、useUpdateMany、useDeleteMany的实现位于 packages/core/src/hooks/data/ 目录,逻辑一致。
再看useLog的源码(packages/core/src/hooks/auditLog/useLog/index.ts):
- 它内部会调用
useGetIdentity获取当前用户,并在调用create时把author: identityData ?? authorData?.data合并进参数——这就是事件中author字段的来源; - 它还读取
resource?.meta?.audit并调用hasPermission进行按 mutation 类型的过滤(详见下文"按资源启用/禁用审计"一节); log使用useMutation包装auditLogContext.create,rename使用useMutation包装auditLogContext.update,并定义了各自的 mutationKey。
Hook 集成:useLog 与 useLogList
除自动审计外,Refine 还提供两个 hook 让你在任意组件中手动读写审计日志(见 useLog 与 useLogList)。
useLog:手动创建 / 重命名审计事件
useLog返回两个 mutation:log与rename。
import { useLog } from "@refinedev/core"; const { log, rename } = useLog();log(创建事件):底层调用auditLogProvider的create方法。
const { log } = useLog(); const { mutate } = log; mutate({ resource: "posts", action: "create", author: { username: "admin", }, data: { id: 1, title: "New post", }, meta: { id: 1, }, });logmutation 的属性如下:
| Property | Type | 说明 |
|---|---|---|
| resource(必填) | string | 资源名称,如"posts" |
| action(必填) | string | 动作名称,如"create" |
| author | Record<string, any> | 操作者信息 |
| meta | Record<string, any> | 附加元数据(如记录 id) |
| data | Record<string, any> | 变更后的数据 |
| previousData | Record<string, any> | 变更前的数据(可选) |
log与rename的类型参数均支持TData(继承BaseRecord)、TError(继承HttpError)、TVariables,默认值分别为BaseRecord、HttpError、{};返回值均为 TanStack Query 的UseMutationResult。
rename(更新事件):底层调用auditLogProvider的update方法。
const { rename } = useLog(); const { mutate } = rename; mutate({ id: 1, name: "Updated Name", });renamemutation 的属性:
| Property | Type | 说明 |
|---|---|---|
| id(必填) | BaseKey | 要更新的事件 id |
| name(必填) | string | 新的事件名称 |
值得注意的源码细节:rename的onSuccess中,如果返回值包含resource字段,Refine 会通过queryClient.invalidateQueries使对应资源的list审计查询失效,从而让useLogList的列表自动刷新(见 useLog/index.ts)。因此你的update方法如果返回{ resource: "posts" }之类的对象,就能触发关联列表的重新拉取。
useLogList:查询审计事件列表
useLogList底层调用auditLogProvider的get方法(对应文档 use-log-list):
import { useLogList } from "@refinedev/core"; const postAuditLogResults = useLogList({ resource: "posts", });useLogList的入参属性:
| Property | Type | 默认值 |
|---|---|---|
| resource(必填) | string | 从路由读取的 action |
| action | string | |
| author | Record<string, any> | |
| meta | Record<string, any> | |
| queryOptions | UseQueryOptions<TQueryFnData, TError, TData> |
其返回值是 TanStack Query 的UseQueryResult<{ data: TData }>。
从源码 useLogList/index.ts 可以看到,它使用useQuery包装get,查询 key 由keys().audit().resource(resource).action("list").params(meta)生成;当get未定义(即未配置 auditLogProvider)时,enabled: false且 queryFn 返回Promise.resolve([])——这意味着即使忘记配置 Provider,页面也不会报错。同时retry: false,避免审计查询在失败时无限重试。
自动记录审计的 Supported Hooks
当 mutation 成功后,以下 hook 会自动调用 Audit Log Provider 的create方法(见 documentation/docs/audit-logs/audit-log-provider/index.md 的 Supported Hooks 小节):
| Package | Hooks |
|---|---|
| @refinedev/core | useForm |
| @refinedev/antd | useForm、useModalForm、useDrawerForm、useStepsForm |
| @refinedev/mantine | useForm、useModalForm、useDrawerForm、useStepsForm |
| @refinedev/react-hook-form | useForm、useModalForm、useStepsForm |
以及核心数据 hookuseCreate、useCreateMany、useUpdate、useUpdateMany、useDelete、useDeleteMany(源码位于 packages/core/src/hooks/data/)。
以useUpdate为例,其发送给create的参数会同时包含data(新值)与previousData(缓存旧值),便于服务端做 diff:
const { mutate } = useUpdate(); mutate({ id: 1, resource: "posts", values: { title: "Updated New Title", }, }); // 发送给 Audit Log Provider `create` 的参数: { "action": "update", "resource": "posts", "data": { "title": "Updated New Title", "status": "published", "content": "New Post Content" }, "previousData": { "title": "Title", "status": "published", "content": "New Post Content" }, "meta": { "id": 1 } }useDeleteMany则只发送action与带ids数组的meta:
const { mutate } = useDeleteMany(); mutate({ ids: [1, 2], resource: "posts", }); // 发送给 Audit Log Provider `create` 的参数: { "action": "deleteMany", "resource": "posts", "meta": { "ids": [1, 2] } }按资源按 mutation 类型启用 / 禁用审计
默认情况下,某个资源的create、update、delete操作都会被记录。若只想记录特定类型的操作,可以在资源的meta.audit中声明允许记录的 action 白名单:
<Refine dataProvider={dataProvider(API_URL)} resources={[ { name: "posts", meta: { audit: ["create"], }, }, ]} />上面的配置表示:对于posts资源,只有create操作会生成审计事件,update与delete均不会。
该机制在源码中的落点是useLog的 mutationFn:它通过pickResource找到资源定义,读取resource?.meta?.audit,再用hasPermission(logPermissions, params.action)判断当前 action 是否被允许;未通过时直接return,不调用create(见 useLog/index.ts)。
从零跑通:参考示例
仓库在 examples/audit-log-provider/ 提供了一个可直接运行的完整示例(对应文档末尾的 CodeSandbox 示例),包含:
- 完整的
auditLogProvider实现(create/get/update三个方法); - 注册 Provider 的
<Refine>配置; - 通过
useLog/useLogList手动记录与展示审计事件的界面代码。
该示例中的 hook 组合、Provider 方法与本文讲解一一对应,是理解整套审计机制的最佳实践参照。另外,useLog与useLogList的单元测试位于 packages/core/src/hooks/auditLog/useLog/index.spec.ts 与 packages/core/src/hooks/auditLog/useLogList/index.spec.ts,可作为验证行为边界的参考。
总结
Refine v5 的审计日志体系由三层构成:Provider 层(auditLogProvider的create/get/update,经AuditLogContext注入)、自动层(数据 hook 成功回调中自动调用log?.mutate,自动附带用户身份与previousData)、手动层(useLog的log/rename与useLogList查询)。配合资源级meta.audit白名单,你可以精确控制哪些资源的哪些操作需要留痕;而"审计落库放在服务端"的建议则提醒我们:客户端上报只是采集,可信的审计真相应保存在服务端。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考