mysql中不能自动监听password字段变更,必须用if old.password != new.password显式比对;注意null处理、排序规则一致、避免硬编码trigger_source,快照表须含毫秒时间戳和操作来源,且仅限dml操作。

MySQL 中用 AFTER UPDATE 触发器捕获密码变更
触发器不能直接监听 password 字段“是否被修改”,MySQL 不提供字段级变更检测函数。必须显式比对旧值和新值,否则哪怕只改了邮箱,也会误写一条密码快照。
实操建议:
- 在触发器中用
IF OLD.password != NEW.password THEN ... END IF;判断——注意:若密码是哈希值(如$2b$12$...),需确保字符集和排序规则一致,否则!=可能因末尾空格或 collation 差异返回 false 正向结果 - 避免用
IF OLD.password IS DISTINCT FROM NEW.password(PostgreSQL 语法,MySQL 不支持) - 如果密码字段允许为
NULL,需额外处理OLD.password IS NULL XOR NEW.password IS NULL场景
快照表设计必须包含 updated_at 和 trigger_source
仅存旧密码值意义有限;没有时间戳和操作上下文,审计时无法判断是用户自主修改、管理员重置,还是脚本批量更新。
推荐快照表结构示例:
CREATE TABLE user_password_history (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
password_hash VARCHAR(255) NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
trigger_source ENUM('user_update', 'admin_reset', 'migration') NOT NULL DEFAULT 'user_update',
created_by VARCHAR(64) NULL COMMENT '如 session_id 或 operator_id'
);
关键点:
-
DATETIME(3)提供毫秒级精度,避免同秒内多次修改覆盖时间戳 - 不要依赖触发器内
NOW()—— 它和语句开始时间绑定,高并发下可能失准;改用CURRENT_TIMESTAMP(3)更可靠 -
trigger_source字段不能硬编码为固定字符串,应通过应用层传入(例如用USER()或会话变量@trigger_source),否则所有记录都标成“user_update”就失去区分价值
禁止在触发器里调用存储过程或访问外部服务
MySQL 触发器执行期间处于事务上下文中,任何对外部资源的依赖都会导致事务不可控甚至卡死。常见翻车场景包括:调用 SLEEP()、查远程 API、写文件、发邮件。
安全边界必须守住:
- 只允许 DML 操作(
INSERT INTO user_password_history ...) - 禁止
SELECT查询其他用户表(除非确定无锁竞争,且已加索引) - 绝对不要在触发器中做密码明文解密、日志脱敏等 CPU 密集操作——这些该由应用层完成
- 如果需要关联用户姓名等信息,应在快照表里冗余存储(如
username字段),而非运行时JOIN
PostgreSQL 用户注意:BEFORE vs AFTER 的陷阱
PostgreSQL 支持 BEFORE UPDATE 触发器,但此时 NEW.password 还未写入磁盘,若你在其中修改 NEW.password(比如自动加盐),会影响主表更新结果——这不是你想要的“记录快照”,而是“篡改业务逻辑”。
正确姿势:
- 一律用
AFTER UPDATE,确保主表已落盘,快照数据与最终状态严格一致 - PostgreSQL 中比较哈希值可用
OLD.password IS DISTINCT FROM NEW.password,它天然处理NULL,比!=更健壮 - 若使用
pgcrypto等扩展生成哈希,请确认触发器中不重复调用gen_salt()——快照只需存结果,不参与计算











