首页
/ Strapi 审计日志(Audit Logs)深度解析:基于 EventHub 的操作审计、保留策略与 API 实现

Strapi 审计日志(Audit Logs)深度解析:基于 EventHub 的操作审计、保留策略与 API 实现

2026-09-05 09:28:20作者:范靓好Udolf

本篇指南以 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.createentry.updateentry.deleteentry.publishentry.unpublish
媒体 media.createmedia.updatemedia.deletemedia-folder.createmedia-folder.updatemedia-folder.delete
用户 user.createuser.updateuser.delete
认证 admin.auth.successadmin.logout
内容建模 content-type.createcontent-type.updatecontent-type.deletecomponent.createcomponent.updatecomponent.delete
权限体系 role.createrole.updaterole.deletepermission.createpermission.updatepermission.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 返回的接口需实现 saveEventfindManyfindOnedeleteExpiredEvents 四个方法。

对照当前源码 services/audit-logs.ts,这四个能力全部落地:

  • saveEvent(event):把事件写入数据库——将 userId 字段改名为 user(对应内容类型的关系字段)后,调用 strapi.db.query('admin::audit-log').createL36-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-managercontent-type-builder 均设为 visible: false——这是一个系统内部表,不暴露给内容管理器界面。

另外,ee/server/src/index.ts 中有一条注释明确说明:audit-log 内容类型始终注册,与功能是否开启无关,目的是防止许可失效后表结构被移除造成数据丢失。

订阅全部事件与写入过滤

Audit Logs 通过 strapi.eventHub.subscribe(handleEvent) 订阅了 EventHub 上的所有事件(lifecycles.ts L158),但并非每个事件都会落库。processEventL78-L112)实现了三道过滤:

  1. 来源过滤:只审计“来自管理员鉴权上下文”的动作,即 requestContextstate.route.info.type === 'admin' 的请求;此外,被标记 auditSource: 'mcp' 的 MCP 触发的管理动作也会被审计;普通 API Token / 公开请求触发的事件一律跳过,且要求请求上下文中存在 user
  2. 事件白名单:事件名必须存在于 eventMap(即 defaultEvents 映射表),否则忽略;
  3. UID 黑名单plugin::upload.fileplugin::upload.folder 两个 uid 对应的事件被显式排除(L101-L104)。

通过过滤后,生成形如 { action, date, payload, userId } 的记录,其中 date 取事件处理时刻的 ISO 时间,payload 中还会附加一个 origin 字段(adminmcp),用于区分操作来源。用户信息则从 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

保留天数并非写死的常量,getRetentionDaysL46-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 就是 nullcore/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.tsstrapi.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:ASCaction:DESCdate:ASCdate:DESC 四个值(防止任意字段排序)。

列表查询的性能优化findMany 采用两阶段查询(services/audit-logs.ts L47-L78)——先只 select: ['id'] 分页取出 ID,再按 ID 批量回填 actiondatepayloadpopulate 用户。源码注释说明,这样做是因为对整行排序会在 MySQL/MariaDB 上耗尽排序内存(对应上游 issue strapi/strapi#27399)。返回前,用户对象会经 getSanitizedUserL14-L28)脱敏,仅保留 idemaildisplayName(优先 username,其次“名 姓”,兜底 email)。

findManyUsers 则通过审计表用户关系表的 join table 做 DISTINCT 统计出所有产生过日志的用户 ID,再分页查询 admin::user 返回(L80-L113),供管理面板按操作人筛选。此外,findOne 在返回详情后会发送 didWatchAnAuditLog 遥测事件(controllers/audit-logs.ts L26-L35)。

功能启停的完整链路

将前面各部分串起来,审计日志在应用启动时的注册链路为(ee/server/src/index.ts):

  1. audit-log 内容类型无条件注册(防止数据丢失);
  2. 计算 isAuditLogsEnabled = admin.auditLogs.enabled(默认 true) && strapi.ee.features.isEnabled('audit-logs')
  3. 启用时:注册 audit-logs 服务与 audit-logs-lifecycle 生命周期服务,并调用 lifecycle.register()——订阅 EventHub、注册午夜清理 cron;
  4. 同时按条件挂载控制器与路由;
  5. 应用销毁时 destroy() 会解除 EventHub 订阅并 cron.remove('deleteExpiredAuditLogs')

参考路径汇总

适用前提说明:Audit Logs 属于 Strapi 的企业版(EE)功能,其生效前提是许可中启用了 audit-logs 功能;自托管企业版默认保留 90 天,云项目的保留上限以许可为准。本文所有实现细节均以当前仓库源码为准。

登录后查看全文
热门项目推荐
相关项目推荐