返回文章列表

Rust 后台系统里,审计日志不是普通日志

203·2 分钟阅读
Rust微服务

普通日志回答“系统发生了什么”,审计日志回答“谁在什么时候,对什么对象,做了什么决定”。

很多后台系统都有日志,但没有审计日志。

管理员改了用户权限,客服导出了订单,财务发起了退款,运营下架了商品。出了问题以后,如果只能在普通应用日志里搜索,很快就会发现信息不完整:没有操作者、没有操作对象、没有前后值、没有请求来源、没有审批单号。

审计日志不是给程序员排错的,是给业务追责和风险复盘的。

本文代码环境:Rust edition 2024,示例只使用标准库。


审计日志要有固定结构

普通日志可以自由一些,审计日志不行。

它至少要回答六个问题:

  • 谁操作
  • 在哪个租户
  • 对什么对象
  • 做了什么动作
  • 结果是什么
  • 请求来源是什么

用 Rust 结构表达:

#[derive(Debug)]
struct AuditLog {
    actor_id: u64,
    tenant_id: Option<u64>,
    action: AuditAction,
    resource: AuditResource,
    result: AuditResult,
}

动作和资源也不要只用字符串:

#[derive(Debug)]
enum AuditAction {
    GrantRole,
    RefundOrder,
    ExportData,
}
 
#[derive(Debug)]
enum AuditResource {
    User(u64),
    Order(u64),
    Report(String),
}

结构化以后,后面查询、告警、报表、合规导出才有基础。


只记录成功操作是不够的

审计日志不仅要记录成功,也要记录失败。

比如有人连续尝试导出不属于自己租户的数据,最后都被拒绝了。这不是业务成功,但它是安全信号。

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AuditResult {
    Succeeded,
    Denied,
    Failed,
}
 
fn should_alert(result: AuditResult, action: AuditAction) -> bool {
    matches!((result, action), (AuditResult::Denied, AuditAction::ExportData))
}

失败审计不能只写“操作失败”。要记录失败类型:权限拒绝、参数非法、目标不存在、系统错误。

我的偏好是:高风险操作无论成功失败都要审计。


前后值要谨慎记录

权限、金额、状态变更通常需要记录前后值。

但不是所有字段都能原样记录。密码、token、身份证、手机号、密钥,都需要脱敏或禁止进入审计日志。

可以把变更值建模:

#[derive(Debug)]
struct FieldChange {
    field: &'static str,
    before: RedactedValue,
    after: RedactedValue,
}
 
#[derive(Debug)]
enum RedactedValue {
    Plain(String),
    Masked,
}

这比直接塞 JSON 更安全。

审计日志本身也可能成为敏感数据源。记录得越全,越要控制访问权限和保留周期。


审计写入失败不能悄悄吞掉

高风险操作有一个现实问题:业务操作成功了,审计日志写入失败,怎么办?

不同场景答案不同。

操作 审计失败策略
普通资料修改 业务成功,后台补写审计
权限授予 审计失败则操作失败
数据导出 审计失败则操作失败
退款 至少写入本地审计 Outbox

可以把策略写清楚:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AuditPolicy {
    BestEffort,
    MustRecord,
}
 
fn policy_for(action: AuditAction) -> AuditPolicy {
    match action {
        AuditAction::GrantRole | AuditAction::ExportData => AuditPolicy::MustRecord,
        AuditAction::RefundOrder => AuditPolicy::MustRecord,
    }
}

不是所有审计都要阻塞业务,但关键审计不能悄悄丢。


审计日志要防篡改

审计日志如果能被普通管理员随便修改,就失去了意义。

至少要做到:

  • 业务表和审计表权限分离
  • 审计日志只追加,不更新
  • 删除走保留策略,不走人工随手删
  • 高风险系统可以做哈希链或写入独立存储

简单的哈希链模型:

#[derive(Debug)]
struct AuditRecord {
    id: u64,
    payload_hash: String,
    previous_hash: String,
}
 
fn chain_input(record: &AuditRecord) -> String {
    format!("{}:{}:{}", record.id, record.previous_hash, record.payload_hash)
}

这不是说每个系统都要做区块链式审计。重点是:审计日志不能和普通业务数据一样随便改。


审计查询也是产品能力

很多团队写了审计表,却没有好用的查询入口。

真正需要时,才发现只能让开发写 SQL。

审计查询至少支持:

  • 按操作者查
  • 按租户查
  • 按资源查
  • 按动作查
  • 按时间范围查
  • 按失败和拒绝查
#[derive(Debug, Default)]
struct AuditQuery {
    actor_id: Option<u64>,
    tenant_id: Option<u64>,
    action: Option<AuditAction>,
    result: Option<AuditResult>,
}

审计不是写进去就完事。查不出来、看不懂、导不出,就很难在风险复盘里发挥作用。


结论

审计日志不是普通应用日志的另一种叫法。它服务的是追责、合规、风控和业务复盘。

审计日志要结构化;高风险操作成功失败都要记录;前后值要脱敏;关键审计写入失败不能静默忽略;审计数据要只追加、防篡改、可查询。

Rust 的类型系统很适合把审计动作、资源和策略写清楚,不要把这些关键字段埋在自由文本里。