SaaS环境审计日志隔离方案 - 第2节

审计日志的分类

说起审计日志,我最早接触是在一个金融 SaaS 项目里。那会儿甲方要求所有用户行为都得留下痕迹,连翻看菜单都要记一笔。一开始觉得有点过度,后来出了个内部人员误删生产数据的事故,幸好日志完整,十分钟就定位到人。嗯,从那以后,我对审计日志再也不敢马虎了。

按用途区分四种核心类型

审计日志不是「一种」日志,它按照触发场景和记录目的可以分为四个大类。我在不同项目里见过五花八门的命名,但归根结底逃不出这几种:

类型 主要用途 典型事件举例
访问日志 记录谁在什么时间访问了什么资源 用户登录、API 调用、页面浏览
操作日志 记录具体的数据变更行为 创建/修改/删除订单、修改密码
异常日志 记录系统错误、业务校验失败等非预期情况 鉴权失败、请求超时、数据库连接池耗尽
安全事件日志 记录与安全直接相关的高风险行为 多次登录失败、权限提升、异常 IP 访问

你可能会问:操作日志和安全事件日志有什么区别?说白了,操作日志重在「改了什么」,安全事件日志重在「可疑行为」。比如一个普通用户改了自己的昵称,记一笔操作日志就够了;但同一 IP 半小时内改了 50 次昵称,那就要往安全事件日志里扔一条告警。

混用还是分开?

我建议在日志收集层就按类型打上 event_type 标签,写入同一套存储系统,但分别建索引或路由到不同的物理队列。这样可以兼顾查询效率和分析灵活度。我记得有个项目把所有日志一股脑写进同一个 Elasticsearch 索引,结果安全分析时被大量业务日志淹没,查找高危事件像大海捞针。后来改了分区策略才解决。

小技巧: 在日志采集代理(如 Filebeat、Vector)里通过正则或关键词快速分类,避免侵入业务代码。

核心字段规范

字段规范是日志能被机器解析和人工排查的基础。我见过最惨的一次事故,是因为时间戳用了本地时间没带时区,跨区域排查时大家对着日志各说各的,整整浪费了半天。从那以后,我对字段规范的要求近乎偏执。

必选字段清单

以下字段我要求每个 SaaS 项目都必须有,缺一个就拒绝上线:

字段名 类型 说明与要求
timestamp ISO 8601 精确到毫秒,必须带时区(如 2025-03-17T14:23:11.123Z)
user_id String 用户唯一标识,不能为空;匿名请求用 anonymous
event_type 枚举 access / operation / exception / security
action String 具体动作,推荐动词加名词组合,如 order.create
source_ip IPv4/IPv6 请求来源 IP,若通过代理透传还需 x_forwarded_for
resource String 被操作的资源路径或标识,如 /api/v1/orders/12345
result String 成功 / 失败 / 拒绝;失败时附带 error_code

这些字段组合起来,基本能回答「谁在什么时候从哪做了什么,结果怎样」。

扩展字段与上下文

必选字段之外,我习惯再塞几个上下文属性,它们能大幅提升排查效率:

  • session_id —— 串联一次会话中的多次操作
  • request_id —— 微服务间链路追踪
  • user_agent —— 客户端类型识别
  • payload_diff —— 操作日志里记录变更前后的值(敏感字段要脱敏)
  • tenant_id —— 多租户场景下必备,否则没法做租户隔离查询

看一个实际的生产 JSON 日志片段:

{
  "timestamp": "2025-03-17T08:12:34.567Z",
  "user_id": "u_abc123",
  "event_type": "operation",
  "action": "order.update",
  "source_ip": "203.0.113.42",
  "resource": "/api/v1/orders/ORD-20250317-001",
  "result": "success",
  "tenant_id": "t_enterprise_01",
  "request_id": "req-xyz789",
  "payload_diff": {
    "before": { "status": "pending", "amount": 199.00 },
    "after":  { "status": "paid",    "amount": 199.00 }
  }
}

你看,这个 payload_diff 直接告诉排查人员改了什么字段,不需要去数据库查 before image,省了不少功夫。

注意: 敏感数据(密码、身份证、银行卡号)不能在 payload_diff 中原样记录,必须脱敏或直接跳过。

记录原则与最佳实践

记录原则听起来像老生常谈,但我踩过的坑告诉我,每一条都是用真金白银换来的。

完整性——不漏记

审计日志不能只记成功不求失败。我曾经接手过一个系统,登录失败日志默认被 try-catch 吞掉了,结果发生了暴力破解事件,日志里一条失败记录都没有,完全没法溯源。正确的做法是:在网关或统一拦截器里,不管业务返回什么,都把请求上下文先扔到日志管道。用个装饰器模式或者 AOP 就能做到。

不可篡改——防删改

日志记录之后,任何人都不能「悄悄」修改或删除它。怎么做?

  • 日志写入后立即设置只读权限(操作系统级别或对象存储策略)。
  • 使用写入日志文件后即时上传到远程不可变存储(如 S3 的 Object Lock)。
  • 对关键日志行计算哈希后上链或存储到专门的审计数据库,定时校验完整性。
避坑: 别只依赖数据库的 update 权限控制。DBA 如果有超级账户依然能改。我曾经在项目里用「日志先写本地文件,由独立守护进程加密上传到 WORM 存储」的方式,才算真正解决了篡改问题。

时序一致性——可回放

日志的时间戳必须单调递增,不能出现时间跳跃(除了时钟校正)。我建议所有日志都在服务端打时间戳,不要信任客户端的时钟。如果日志需要跨数据中心聚合,使用 NTP 同步,并且记录时带上时钟漂移偏移量,以便事后校准。

保留策略——存多久?

不同日志类型保留时间不同。合规要求通常强制至少保留 6 个月,金融类可能要求 2 年以上。我习惯的做法:

  • 热索引(7 天):用于日常告警和实时分析,存 Elasticsearch。
  • 温存储(1~3 个月):用便宜点儿的对象存储,配合 Presto 或 Athena 查询。
  • 冷归档(1~3 年):压缩后存 Glacier,仅用于合规审计。

下面这张图可以帮你快速记住审计日志的核心结构:

审计日志核心要素结构图 审计日志核心要素 日志类型 访问日志 操作日志 异常日志 安全事件日志 字段规范 timestamp (ISO 8601) user_id event_type + action source_ip resource + result 记录原则 完整性 不可篡改 时序一致性 保留策略

三个模块互相支撑:日志类型决定字段模板,字段规范保证数据可解析,记录原则确保数据可信可用。你设计审计方案时,最好把这三个维度都列出来逐一检查。

好了,关于审计日志的基础分类、字段规范和记录原则,我们今天就聊到这。记住一点:日志写得好不好,直接决定了出事之后你是一脸懵还是胸有成竹。动手之前先把规范定死,后面省心无数。