Strapi 审计日志(Audit Logs)深度解析:基于 EventHub 的操作审计、保留策略与 API 实现
本篇指南以 Strapi 官方文档 Audit Logs(审计日志)为骨架,结合 packages/core/admin/ee 下的真实源码,系统讲解审计日志功能的后端设计:它如何通过 EventHub 订阅全应用事件并筛选出默认事件清单、strapi_audit_logs 内容类型的完整 Schema、保留天数(retention days)的决策逻辑与每日午夜清理任务的实现,以及管理端 API 的路由、鉴权与查询优化细节。读完后,你将能够理解该 EE 功能从“事件发射”到“入库、查询、过期删除”的完整链路,并知道如何通过 config/admin 配置 admin.auditLogs.retentionDays 等选项来自定义行为。
功能概览:记录哪些管理员操作
Audit Logs 提供在 Admin API 层面查看全部用户操作历史的能力。它覆盖的动作包括:
- 条目(entries)操作,包括发布(publish)与取消发布;
- 媒体(media)及其文件夹(media folders)操作;
- 用户(users)的创建、更新、删除;
- 管理员的登录与登出(login & logout);
- 组件(components)、内容类型(content types)、角色(roles)与权限(permissions)的增删改。
哪些事件会被真正记录,由源码中的 defaultEvents 清单精确界定,完整列表见 lifecycles.ts:
| 分类 | 事件名 |
|---|---|
| 条目 | entry.create、entry.update、entry.delete、entry.publish、entry.unpublish |
| 媒体 | media.create、media.update、media.delete、media-folder.create、media-folder.update、media-folder.delete |
| 用户 | user.create、user.update、user.delete |
| 认证 | admin.auth.success、admin.logout |
| 内容建模 | content-type.create、content-type.update、content-type.delete、component.create、component.update、component.delete |
| 权限体系 | role.create、role.update、role.delete、permission.create、permission.update、permission.delete |
需要注意的是,EventHub 中流转的事件远不止这些——审计日志只落库上表中的默认事件,其余事件会被静默忽略(详见下文“订阅全部事件”一节)。
后端整体设计
官方文档指出,Audit Logs 功能构建在 Strapi 的 EventHub 之上(EventHub 的完整 API 说明见 event-hub.mdx),核心服务代码位于 packages/core/admin/ee/server/src/audit-logs。从源码结构看,其内部拆分为四层:
content-types/:定义存储用的admin::audit-log内容类型;services/:audit-logs.ts负责数据库交互,lifecycles.ts负责事件订阅与定时清理的生命周期管理;controllers/+routes/+validation/:对外暴露的管理端 REST API;- 入口 ee/server/src/index.ts 负责按许可(license)状态注册或跳过整套服务。
审计日志本地 Provider
文档描述的本地 Provider(local provider)是负责与数据库交互的抽象:它应返回一个带 register 函数的对象,register 返回的接口需实现 saveEvent、findMany、findOne、deleteExpiredEvents 四个方法。
对照当前源码 services/audit-logs.ts,这四个能力全部落地:
saveEvent(event):把事件写入数据库——将userId字段改名为user(对应内容类型的关系字段)后,调用strapi.db.query('admin::audit-log').create(L36-L45);findMany(query):分页查询日志列表,先取 ID 再回填字段(原因见下文“查询优化”);findOne(id):按 ID 查询单条日志并populate用户;deleteExpiredEvents(expirationDate):删除早于过期时间的全部记录(L133-L141)。
服务通过 strapi.add('audit-logs', ...) 注册为内部服务,生命周期服务则注册为 audit-logs-lifecycle,见 index.ts。
内容类型 strapi_audit_logs
strapi_audit_logs 内容类型负责存储所有审计日志,每一个被允许的事件对应表中的一条记录。其 Schema 定义在 content-types/audit-log.ts,完整结构如下:
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
action |
string | 是 | 事件名称,如 entry.publish |
date |
datetime | 是 | 事件发生时间(ISO 字符串) |
user |
relation(oneToOne → admin::user) |
否 | 触发事件的管理员用户 |
payload |
json | 否 | 事件的附加信息(条目 ID、媒体 ID 等) |
Schema 中还有两个值得注意的细节(L10-L20):
options.timestamps: false——关闭框架自动的createdAt/updatedAt,时间统一使用业务字段date;pluginOptions中将content-manager与content-type-builder均设为visible: false——这是一个系统内部表,不暴露给内容管理器界面。
另外,ee/server/src/index.ts 中有一条注释明确说明:audit-log 内容类型始终注册,与功能是否开启无关,目的是防止许可失效后表结构被移除造成数据丢失。
订阅全部事件与写入过滤
Audit Logs 通过 strapi.eventHub.subscribe(handleEvent) 订阅了 EventHub 上的所有事件(lifecycles.ts L158),但并非每个事件都会落库。processEvent(L78-L112)实现了三道过滤:
- 来源过滤:只审计“来自管理员鉴权上下文”的动作,即
requestContext中state.route.info.type === 'admin'的请求;此外,被标记auditSource: 'mcp'的 MCP 触发的管理动作也会被审计;普通 API Token / 公开请求触发的事件一律跳过,且要求请求上下文中存在user; - 事件白名单:事件名必须存在于
eventMap(即defaultEvents映射表),否则忽略; - UID 黑名单:
plugin::upload.file与plugin::upload.folder两个 uid 对应的事件被显式排除(L101-L104)。
通过过滤后,生成形如 { action, date, payload, userId } 的记录,其中 date 取事件处理时刻的 ISO 时间,payload 中还会附加一个 origin 字段(admin 或 mcp),用于区分操作来源。用户信息则从 strapi.requestContext.get().state.user 获取——这正是官方文档所述“从 requestContext 中获取用户”的实现。
许可状态变化同样由事件驱动:register() 监听 ee.enable / ee.update / ee.disable 三类事件,在许可更新时销毁并重建服务以读取新的许可信息,在许可关闭时停止订阅(L123-L150)。
保留天数(Retention Days)与每日清理任务
审计日志数量会持续增长,因此框架注册了一个名为 deleteExpiredAuditLogs 的 cron 任务,Cron 表达式为 '0 0 * * *',即每天午夜执行一次,删除日期早于“当前时间 - 保留天数”的记录(L163-L171)。Strapi Cron Service 的通用用法可参考 cron.mdx。
保留天数并非写死的常量,getRetentionDays(L46-L64)的决策逻辑与官方文档描述完全一致:
const DEFAULT_RETENTION_DAYS = 90;
const getRetentionDays = (strapi: Core.Strapi) => {
const featureConfig = strapi.ee.features.get('audit-logs');
const licenseRetentionDays =
typeof featureConfig === 'object' && featureConfig?.options?.retentionDays;
const userRetentionDays = strapi.config.get('admin.auditLogs.retentionDays');
// 企业自托管:许可未指定保留天数时,用用户配置,否则默认 90 天
if (licenseRetentionDays == null) {
return userRetentionDays ?? DEFAULT_RETENTION_DAYS;
}
// 云项目:用户可自定义,但不能超过许可定义的上限
if (userRetentionDays && userRetentionDays <= licenseRetentionDays) {
return userRetentionDays;
}
// 用户未提供时,直接使用许可值
return licenseRetentionDays;
};
用表格概括三种情形:
| 场景 | 许可 retentionDays | 用户配置 admin.auditLogs.retentionDays |
实际生效值 |
|---|---|---|---|
| 企业自托管(许可未设上限) | null |
未设置 | 90 天(DEFAULT_RETENTION_DAYS) |
| 企业自托管 | null |
已设置(如 30) | 30 天 |
| 云项目 | 例如 365 | 未设置 | 365 天(许可值) |
| 云项目 | 例如 365 | 200(≤365) | 200 天 |
| 云项目 | 例如 365 | 400(>365) | 365 天(许可上限兜底) |
从源码看,默认许可模板中 audit-logs 功能的 retentionDays 就是 null(core/src/ee/license.ts),即自托管企业版天然走“用户配置优先、默认 90 天”的分支;而云项目的上限由下发的许可携带。
配置方式:修改 Admin Panel API 配置文件 config/admin.js(或 TS 项目中的 config/admin.ts,可参考 examples/complex/config/admin.ts),在返回对象中增加 auditLogs 字段:
const adminConfig = ({ env }) => ({
auditLogs: {
enabled: true, // 功能开关,默认即为 true
retentionDays: 30, // 保留天数;自托管可任意设置,云项目受许可上限约束
},
});
export default adminConfig;
其中 enabled 选项的读取逻辑在 index.ts:strapi.config.get('admin.auditLogs.enabled', true) 且 strapi.ee.features.isEnabled('audit-logs') 两者同时成立时,才会注册审计日志的控制器与路由。
审计日志的数据格式
每条审计日志遵循如下格式(官方文档原文定义):
type Event {
action: string, // 事件名称
date: Date, // 事件发生时间
userId: number, // 触发事件的用户 ID
payload?: Object, // 事件的附加信息
};
源码中的实际接口定义与之一致(services/audit-logs.ts L3-L8),入库时 userId 改存为关系字段 user。
理解这个格式需要知道 EventHub 如何发射事件。发射事件时调用:
strapi.eventHub.emit(name: Pick<Event, 'name'>, payload: Pick<Event, 'payload'>);
第一个参数是事件名(对应 action),第二个是 payload。审计日志订阅回调 handleEvent(name, ...args) 收到事件后,先用 eventMap 中的 payload 构造函数(默认事件取 args[0] 作为 payload)补齐 payload,再从 requestContext 取出 user.id 作为 userId——即“先校验事件来自 admin 请求且在默认事件清单内,再从发射事件中提取 action 与 payload”的完整流程。
管理端 API:路由、鉴权与查询
审计日志对外暴露三个只读路由(routes/audit-logs.ts),全部挂载在 Admin API 下:
| 方法 | 路径 | Handler | 说明 |
|---|---|---|---|
| GET | /admin/audit-logs |
audit-logs.findMany |
分页查询日志列表 |
| GET | /admin/audit-logs/users |
audit-logs.findManyUsers |
查询出现过日志的用户列表(供前端筛选器使用) |
| GET | /admin/audit-logs/:id |
audit-logs.findOne |
查询单条日志详情 |
三条路由共用同一套安全配置(L3-L14):
enableFeatureMiddleware('audit-logs')中间件——功能未启用时直接拒绝;admin::isAuthenticatedAdmin策略——必须是已登录的管理员;admin::hasPermissions策略并要求admin::audit-logs.read权限——可在角色权限中单独控制谁能查看审计日志。
查询参数校验由 yup schema 完成(validation/audit-logs.ts):
page:整数,最小 1;pageSize:整数,1 到 100;sort:仅允许action:ASC、action:DESC、date:ASC、date:DESC四个值(防止任意字段排序)。
列表查询的性能优化:findMany 采用两阶段查询(services/audit-logs.ts L47-L78)——先只 select: ['id'] 分页取出 ID,再按 ID 批量回填 action、date、payload 并 populate 用户。源码注释说明,这样做是因为对整行排序会在 MySQL/MariaDB 上耗尽排序内存(对应上游 issue strapi/strapi#27399)。返回前,用户对象会经 getSanitizedUser(L14-L28)脱敏,仅保留 id、email 与 displayName(优先 username,其次“名 姓”,兜底 email)。
findManyUsers 则通过审计表用户关系表的 join table 做 DISTINCT 统计出所有产生过日志的用户 ID,再分页查询 admin::user 返回(L80-L113),供管理面板按操作人筛选。此外,findOne 在返回详情后会发送 didWatchAnAuditLog 遥测事件(controllers/audit-logs.ts L26-L35)。
功能启停的完整链路
将前面各部分串起来,审计日志在应用启动时的注册链路为(ee/server/src/index.ts):
audit-log内容类型无条件注册(防止数据丢失);- 计算
isAuditLogsEnabled = admin.auditLogs.enabled(默认 true) && strapi.ee.features.isEnabled('audit-logs'); - 启用时:注册
audit-logs服务与audit-logs-lifecycle生命周期服务,并调用lifecycle.register()——订阅 EventHub、注册午夜清理 cron; - 同时按条件挂载控制器与路由;
- 应用销毁时
destroy()会解除 EventHub 订阅并cron.remove('deleteExpiredAuditLogs')。
参考路径汇总
- 功能文档:docs/docs/docs/01-core/admin/01-ee/02-audit-logs.md
- 事件订阅、默认事件清单、保留天数与 cron:packages/core/admin/ee/server/src/audit-logs/services/lifecycles.ts
- 数据库交互服务:packages/core/admin/ee/server/src/audit-logs/services/audit-logs.ts
- 内容类型 Schema:packages/core/admin/ee/server/src/audit-logs/content-types/audit-log.ts
- 路由 / 控制器 / 校验:packages/core/admin/ee/server/src/audit-logs/routes/audit-logs.ts、packages/core/admin/ee/server/src/audit-logs/controllers/audit-logs.ts、packages/core/admin/ee/server/src/audit-logs/validation/audit-logs.ts
- EE 入口与功能门控:packages/core/admin/ee/server/src/index.ts
- 默认许可配置:packages/core/core/src/ee/license.ts
- EventHub / Cron API 文档:docs/docs/api/event-hub.mdx、docs/docs/api/cron.mdx
适用前提说明:Audit Logs 属于 Strapi 的企业版(EE)功能,其生效前提是许可中启用了 audit-logs 功能;自托管企业版默认保留 90 天,云项目的保留上限以许可为准。本文所有实现细节均以当前仓库源码为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00