ドキュメント目次

审计与留痕

本文说明该系统用全站操作日志与业务流水两层留痕支撑事后审计:能回答“谁改过、何时改”,但改前旧值、读取与导出行为、可信来源地址均未被记录。

  • 全站日志只记写操作,查询与整表导出不留痕
  • 变更前旧值基本为空,仅删除动作有快照
  • 日志默认九十天随服务启动自动物理删除
  • 改前版本靠工艺版次、审批、单据快照等业务流水
  • 导出无额外权限且不留痕,需制度补位

18.1 出了事,能不能证明当时该做的都做了

老葛有一次被老陈叫进办公室:一批活的尺寸不对,客户索赔,问"到底是谁改的工艺"。

老葛说不是我。老陈说,那你能不能拿出点东西来。

这就是这一章要回答的唯一一个问题:事情过去之后,系统里还剩下什么。剩下的是"某个人某天改过",还是"当时是什么、后来变成什么、为什么变"。前者叫日志,后者叫证据,两者差得很远。

这套系统在留痕这件事上做了两层:全站自动记一层,业务里各记一层。 两层都有实打实的用处,也各有一处不能不知道的形状。

18.2 全站那一层:谁、什么时候、动了哪张单

所有写操作都被自动记一条。记录的字段的确丰富:

  • 动作是语义化的,不是请求方法名。库里在用的动作有创建、修改、删除、导入、导出、登录、登录失败、审批通过、驳回、提交、转生产、取消、调拨、上传、查询、追溯这十几类,由一张路径到动作的映射表推出来;映射表没覆盖的,按请求方法回退成增改删。
  • 对象定位到业务号:哪个模块、哪类资源、哪条记录的主键、还有一个可读名称(订单取单号、客户取客户名、物料取名称),为的是审计时不用对着数字猜。
  • 人这一头记四样:操作人编号、用户名、来源地址、浏览器标识。
  • 结果这一头记三样:成功还是失败、失败信息、耗时与响应码。连失败的请求都留——这一点很要紧,"我试了但没改成"和"我改成了"在追溯时是两回事。
  • 请求内容原样存一段,最长一万个字符,超出截断;不是可解析格式的二进制存两千字符。

有一处细节值得单独点出来:密码类字段是打星存的。七个键名(新旧密码、确认密码、令牌、密钥之类)在入库前被替换成星号,注册、改密、重置这三个动作也在映射表里。审计要的是"某人改过密码"这个事实,不是那串字符。

写入不卡在请求路径上——请求先走完,日志另起线程落库;日志写失败就丢这一条,不把业务一起带倒。这个取舍是对的:留痕不该以打断生产为代价。

18.3 三个缺口,都在这层

先说最要命的:只记写,不记读。

自动记录那一层明确跳过所有查询类请求。所以"谁在什么时候看了什么"没有答案。往深里说一层:整表导出这个动作也不留痕——导出走的是查询类请求,跳过了。谁把全部客户和联系方式带走了一份,这套系统答不上来。这是选型时要按实说清的一条,不是小节的修辞。

(有一个例外:追溯查询这类接口本身是用写入方式调的,所以留了痕。例外恰恰证明规律——留不留痕取决于它碰巧是哪种请求,不是取决于该不该记。)

第二个缺口:"改前是什么"这一格基本是空的。

记录表里其实设计了三格:变更前、变更后、变更摘要。本轮只读核对本地库那两千八百多条,实况是这样:

有内容的条数
变更前250 条,全部是删除
变更后0 条
变更摘要0 条
请求内容约六成

原因不是执行坏了,是没人往这三格写。删除动作有一个专门的快照:在真正删之前,把那条记录抓下来存进"变更前"。而修改动作只存了"你提交了什么"——改之前的旧值不在里面。表上还留着一个能做前后对比的装饰器,全仓只标了一处,那一条路径也没走到过。

于是 18.1 那个问题要照实回答:"这批活的工艺被谁改过",日志能答;"改之前那一版是什么",日志答不了。 能答的那一半靠后面一节讲的第三层。

删除的快照也不是每次都抓到。要满足两个条件:这条路径在映射表里认得出资源类型,且能从路径里解析出记录编号。认不出的那些删除,删完就没影了。本地库那 501 条删除里,带快照的是 250 条。

第三个缺口:来源地址那一格可能是假的。

存进去的地址取的是"直连你的那一头",全仓没有任何一处去读转发头。开发环境里直连,所以记下来全是本机地址;生产是网关代理到后端,记下来的会是网关容器的地址。审计里的"从哪台机器操作"目前不可信。同一条链路上,还有二十来条写记录没有操作人(免登录那几个口子留下的)。

18.4 日志本身能不能被抹掉

能看日志的接口要权限码:列表、统计、导出,都要"查看日志"这一个码。

同一个码下面还开着清理:一个按日期批量删除的入口,把某时之前的记录物理删掉,没有归档去处、没有二次确认、也没有保留期下限。

比手工清理更该知道的,是还有一条自动的:服务每次启动,会把设定天数之前的日志直接删掉,默认九十天(这个天数是可配的环境变量,而三份部署编排文件里都没配它,所以线上跑的就是九十天)。也就是说,重启一次服务,九十天前的留痕就没了,没有人被通知,也没有地方放着。

三句话讲清这件事:能看日志的人,就能删日志;就算没人动手,九十天之后它自己也会没;系统里没有"日志只增不删"的保证。

还有一处不配套:单条日志的详情接口只要登录就能查。日志里存着请求原文,也就是存着别人改了什么内容——任何拿到一个日志编号的登录用户都能翻出那条内容,这一层没设门槛。

18.5 真正回答"改之前是什么"的,是业务里那些流水

上面这堆缺口不代表厂里查不到旧值。查得到,只是不在操作日志里,在业务表自己的留痕里。这才是这套系统证据链的主体:

  • 客户阶段变更单独一条流水:原阶段、新阶段、谁改的。销售退回客户池、认领、超期提醒,都有迹可查。
  • 审批留下完整历史:模板、节点、实例、每一级谁批的、批时说了什么,四层分开存。第十一章讲的那些审批卡控,事后就是靠这一层复核。
  • 工艺路线是有版次的:每版有生效与停用日期,工单绑定它当时用的那一版。"当时按哪一版干的"这一问,永远答得出——第九章取版收敛就是为这句话做的。
  • 单据在关联主数据时做快照冗余:报价明细把产品规格、单价一并存下,事后主数据改了,历史单据不受影响(第十四章说过这套冗余的代价:家底改动不会传下去)。
  • 状态跃迁另有流水表:前后状态、事件、操作人、备注、时间。它记的是"从哪一格到那一格",与"哪一格被改了什么"是两回事。
  • 检验、不良、报废各留一条自己的记录链:判定人、判定时间、来源工单都写在单上,不是写在日志里(第十二章)。

这一节的结论值得说重:这套系统的可追溯性主要靠业务留痕,不靠操作日志。 好处是证据带着业务含义、查起来顺;代价是每加一类单据,就得自己记得建一张流水表——忘了建,那类单据就没有历史。

18.6 这一章不做的事

  • 不做防篡改存证。 没有写一次不可改的存储、没有哈希链、没有异地归档、没有第三方时间戳。日志表就在业务库里,能进库的人就能改。
  • 不做合规审计包。 没有按审核模板导出的审计底稿。有导出功能,但导出的是当前查到的那些行。上面那条九十天的自动清理,恰恰是反过来的:它保证的是过期的必查不到,不是该留的留得住。
  • 不做告警与行为分析。 异常登录、批量导出、越权尝试,都只是记下,不会有人被通知。
  • 不做配置变更的对比视图。 编号规则、权限、状态机改过没有?日志里有条目,但没有"从什么变成什么"的画面。

18.7 今晚能定的三条

  1. 把"哪些历史必须由业务表留"当成一张清单来管。 已经留痕的六处(客户阶段、审批、工艺版本、单据快照、状态流水、检验判定)是家底;下一类新单据上线前,先问它要不要历史,别指望事后从操作日志里挖出来——那里没有旧值。
  2. 先把保留天数这件事定下来,再谈谁有权删。 默认九十天,意味着季度对账、年度质量回访想看当时的操作记录,事后一重启就没了。厂里要先回答一句:留痕最少要存多久?定了就把那个环境变量写成明面配置,并且让清理动作先归档再删。在改之前,靠制度补一条:每月导出一份日志存档,且没人有权删掉唯一的那份。
  3. 别把导出当无害动作。 现在导出既不需要额外权限点,也不留痕。在补上之前,能导客户清单的名单要单独管,并且把"导出前先报备"写成一条纪律。

这一章能定的事就一句:"谁改过"记得挺全,"改之前是什么"要靠业务表自己的流水,"谁看过"没记。 老葛那次没拿出东西来——不是系统一句都没记,是记的那几样刚好不在他手上,而真正答得出的那几处,当时还没人知道要去查。