跳到主要内容
版本:3.1

AuditLogger

3.0.0 页面说明

  • 3.0.0 说明:当前页已重写为 3.x 的审计日志主题页。
  • 当前组件入口: Aegis.Security.Audit

AuditLogger 用来记录业务操作留痕。它最适合放在 Service 层,把关键动作沉淀成统一的审计记录。

推荐接入路径

第一步:启用组件

{
"Components": {
"Services": [
"Security.Audit"
],
"Middlewares": []
}
}

第二步:注入 AuditLogger<T>

public class SettlementService
{
private readonly AuditLogger<SettlementService> _auditLogger;

public SettlementService(AuditLogger<SettlementService> auditLogger)
{
_auditLogger = auditLogger;
}
}

最常见的两种写法

方式一:每次显式指定操作者

await _auditLogger
.SetOperator("1", OperatorType.医生, "张三")
.SetDomain(Domain.结算单)
.SetContent(OperationType.创建, "SETTLE_001", "创建结算单", "创建结算单 SETTLE_001")
.LogAsync();

适合场景:

  • 操作者信息每次都不同
  • 控制器里临时记一笔审计

方式二:先 PreSetOperator(...)

public class SettlementService
{
private readonly AuditLogger<SettlementService> _auditLogger;

public SettlementService(AuditLogger<SettlementService> auditLogger)
{
_auditLogger = auditLogger;
_auditLogger.PreSetOperator("1", OperatorType.医生, "张三");
}

public async Task CreateAsync()
{
await _auditLogger
.SetDomain(Domain.结算单)
.SetContent(OperationType.创建, "SETTLE_001", "创建结算单", "创建结算单 SETTLE_001")
.LogAsync();
}
}

适合场景:

  • Service 中的操作者通常就是当前登录用户
  • 希望减少重复代码

什么时候要带资源标识

如果你希望后续能直接按资源查审计记录,建议用带 operationResourceId 的重载。

await _auditLogger
.SetOperator("1", OperatorType.医生, "张三")
.SetDomain(Domain.结算单)
.SetContent(OperationType.更新, "SETTLE_001", "更新结算单", "修改结算单金额")
.LogAsync();

什么时候要带变更前后数据

对于“修改类”操作,更推荐补上 SetChangeData(...)

await _auditLogger
.SetOperator("1", OperatorType.医生, "张三")
.SetDomain(Domain.结算单)
.SetContent(OperationType.更新, "SETTLE_001", "更新结算单", "修改结算单金额")
.SetChangeData("金额,状态", newData, oldData)
.LogAsync();

正式项目通常还要做什么

默认情况下,组件会把 IAuditStorage 指到控制台存储。

推荐方案:引入 Aegis.Audit.FreeSql NuGet 包,它会自动注册基于数据库的审计存储和分页查询服务,无需手动编写存储代码。详见 安全审计文档的 FreeSql 章节

如果你需要将审计数据写入消息队列、Elasticsearch 等其他介质,可以自己实现 IAuditStorage

services.AddScoped<IAuditStorage, AuditDatabaseStorage>();

关于来源系统标识(SourceSystem)

每条审计记录都会自动携带 SourceSystem 字段,标识该记录来自哪个系统。填充优先级如下:

  1. Aegis:AppName 配置(推荐)
  2. 当前项目程序集名称Assembly.GetEntryAssembly().GetName().Name,自动 fallback)
  3. "Unknown"(兜底值)
{
"Aegis": {
"AppName": "MyService"
}
}
  • 多系统审计:如果多个系统共用同一个审计数据库,务必为每个系统配置不同的 AppName
  • 查询时筛选:可通过 IAuditQueryServiceSourceSystem 查询条件筛选特定系统的记录
  • 无需手动赋值AuditLogger 在构建审计记录时会自动填充此字段
  • 未配置 AppName:如果未配置 Aegis:AppName,系统自动使用当前项目的程序集名称(即 .csproj 文件名),无需额外配置

关于客户端 IP(ClientIp)

每条审计记录都会自动携带 ClientIp 字段,记录发起本次操作的客户端 IP。无需手动赋值

填充来源是 ActionContext.Current.IpAddress,由 ContextInspector 中间件在每个 HTTP 请求时自动采集:

  1. 优先取 X-Forwarded-For 第一个 IP(最外层代理看到的客户端真实 IP)
  2. 无 XFF 时回退到 RemoteIpAddress(直连场景)
  3. 自动清理 IPv4-mapped IPv6 前缀 ::ffff:

也就是说,只要项目启用了 ContextInspector 中间件,所有审计日志都会自动带上正确的客户端 IP,业务代码无需任何改动。

手动覆盖:WithIp(...)

后台任务、消息消费等非 HTTP 上下文场景下,ActionContext.Current 为 null,ClientIp 会是 null。如果这类场景需要标记来源,可以用 WithIp(...) 手动覆盖:

await _auditLogger
.SetOperator("system", OperatorType.系统, "定时任务")
.SetDomain(Domain.结算单)
.SetContent(OperationType.对账, "批量对账完成")
.WithIp("background-job")
.LogAsync();

WithIp(...) 也可用于 HTTP 场景下强制覆盖自动读取的值(一般不需要)。

关于操作时间(OperationTime)

每条审计记录都会自动携带 OperationTime 字段,默认为构建审计记录时的当前时间DateTime.Now),绝大多数场景无需手动指定。

手动覆盖:WithOperationTime(...)

补录历史操作、异步任务补审计、批量导入等场景下,操作实际发生时间早于记录时间,可用 WithOperationTime(...) 指定真实发生时间:

// 补录昨天的一笔结算操作
await _auditLogger
.SetOperator("1", OperatorType.医生, "张三")
.SetDomain(Domain.结算单)
.SetContent(OperationType.创建, "SETTLE_001", "创建结算单", "补录昨日结算")
.WithOperationTime(yesterday)
.LogAsync();

常见问题

为什么 SetDomain(...) 直接报错

一般是因为你用了预设操作者模式,但没有先调用 PreSetOperator(...)

审计日志应该记在 Controller 还是 Service

更推荐记在 Service。
这样更贴近业务动作,也更容易长期维护。

默认存储能不能直接用于生产

不建议。
正式环境推荐引入 Aegis.Audit.FreeSql 包实现数据库持久化,或自己实现 IAuditStorage

继续阅读