真正防篡改必须靠“不可逆审计日志 + 权限隔离 + 应用层约束”三者协同;BEFORE UPDATE触发器仅能拦截负金额、非法状态跳转、时间倒写等明显违规,对DBA直连、ETL绕过、伪造会话变量等完全无效;AFTER UPDATE才是审计日志唯一可靠时机,须写入独立库并用NULL安全比较。

单靠触发器无法防止历史账单被篡改,它只能在 UPDATE 语句执行前拦截明显非法变更;真正防篡改必须靠“不可逆审计日志 + 权限隔离 + 应用层约束”三者协同。
BEFORE UPDATE 触发器只能拦住这三类明显违规
它对合法权限下的修改完全无感,只适合做“最后一道校验”:
-
amount被设为负数、或超出业务允许范围(如单笔账单 > 1000 万元) -
status从'paid'直接跳到'canceled',跳过了'refunding'中间态 -
created_at或bill_date被手动往前写(如改成'2020-01-01'),违背时间单调性
但以下情况它完全拦不住:
- DBA 用
root@localhost执行UPDATE billing_history SET amount = 99999 WHERE id = 123 - ETL 工具直连数据库批量更新,绕过所有会话变量和应用逻辑
- 应用层伪造
@current_app_user后再更新updated_by字段
AFTER UPDATE 触发器才是审计日志的唯一可靠时机
想让每一次修改都“留痕”,必须用 AFTER UPDATE,原因很实在:
-
BEFORE阶段主表还没落盘,触发器出错会导致整个事务回滚——你既没改成功,也没留下日志 -
AFTER阶段主表已提交,OLD和NEW仍可用(MySQL 8.0+、PostgreSQL 全支持),此时写日志失败不影响主业务(只要兜底策略到位) - 日志表必须独立建库(如
audit_db),避免和业务表争 I/O、锁和内存资源
示例(MySQL):
CREATE TRIGGER tr_billing_history_after_update
AFTER UPDATE ON billing_history
FOR EACH ROW
INSERT INTO audit_db.billing_audit_log (
table_name, operation_type, old_data, new_data, operated_by, operate_time
) VALUES (
'billing_history', 'UPDATE',
JSON_OBJECT('id', OLD.id, 'amount', OLD.amount, 'status', OLD.status),
JSON_OBJECT('id', NEW.id, 'amount', NEW.amount, 'status', NEW.status),
COALESCE(@current_app_user, CURRENT_USER()),
NOW(3)
);
最容易被忽略的三个硬伤
这些点不处理,审计日志就等于形同虚设:
-
OLD.col != NEW.col判断字段是否真改了?别这么写——遇到NULL时整个条件恒为FALSE。MySQL 应用NOT (OLD.col NEW.col),PostgreSQL 用OLD.col IS DISTINCT FROM NEW.col - 多个触发器监听同一事件(比如两个
AFTER UPDATE)?执行顺序按创建时间定,没有显式依赖就等于埋雷 - 触发器里调
NOW()或自定义函数?MySQL 主从复制可能不一致,除非函数声明为DETERMINISTIC,或 binlog 格式设为ROW
真正的防篡改不是靠触发器“拦”,而是靠事后可追溯、不可抵赖、不可删除的日志链——而这条链的起点,必须是 AFTER 且写入独立库的那一刻。

















