触发器不能防篡改,仅能拦截明显违反字段级业务规则的UPDATE操作;它无法识别请求来源、权限上下文或用户角色,仅校验NEW值本身是否合规,如金额为负、状态非法跳变等。

不能靠触发器“防篡改”,它只能拦住明显违反规则的修改;真正防恶意篡改,得靠应用层鉴权 + 行级权限 + 审计日志,触发器只是最后一道数据合法性校验闸门。
BEFORE UPDATE 中用 SIGNAL 拦什么、不拦什么
触发器只对字段值本身的业务规则有效,比如金额不能为负、状态不能跳变、小数位不能超限。它看不到请求来源、JWT、session 或操作人角色。
- 能拦:
UPDATE users SET balance = -100 WHERE id = 123(触发器检查NEW.balance 就报错) - 拦不住:
UPDATE users SET name = 'admin_hack' WHERE id = 123 AND created_by = 'admin'(条件合法,触发器无上下文) - 更拦不住:DBA 直连执行、SQL 注入绕过 ORM、旧服务直连写库
为什么必须用 BEFORE 而不是 AFTER
AFTER UPDATE 触发器里写 SIGNAL 是无效的——数据早已落库,错误只会中断触发器本身,不会回滚那行更新。MySQL 不允许在 AFTER 阶段撤回已提交变更。
- 正确姿势:所有校验逻辑必须放在
BEFORE UPDATE,用IF ... THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'xxx' - 错误典型:
AFTER UPDATE里查日志表再报错——非法值已写死,补救成本远高于预防 - 注意:
NEW和OLD在BEFORE阶段都可读可写,但不建议自动“修复”值(如SET NEW.balance = GREATEST(0, NEW.balance)),应直接报错更清晰
敏感字段只读保护的实际写法
比如 password、salary、created_at 这类字段,禁止任何 UPDATE 修改,最简逻辑就是比对新旧值是否变化。
DELIMITER $$
CREATE TRIGGER protect_salary_update
BEFORE UPDATE ON employees
FOR EACH ROW
BEGIN
IF NEW.salary != OLD.salary THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'salary is read-only';
END IF;
END$$
DELIMITER ;
- 别加多余判断:不需要查
USER()或CURRENT_USER()做权限过滤——连接池复用下它不可靠,且违背分层原则 - 若业务真需“例外通道”(如 HR 重置薪资),应走独立接口或专用函数,而不是在触发器里塞白名单逻辑
- 字段名要进错误消息:
CONCAT('Field ''salary'' is read-only'),方便运维快速定位
容易被忽略的并发与性能陷阱
触发器本身不加额外锁,但它的执行会卡在行锁之后;高并发更新同一行时,MySQL 会串行执行触发器逻辑,所以校验不会失效——但慢也会慢得明显。
- 避免在触发器里调用自定义函数、查其他表、或做字符串正则匹配——MySQL 8.0 前不支持这些,即使支持也极易拖垮性能
- 批量更新(如
UPDATE ... WHERE id IN (1,2,3...))会逐行触发,10 万行 = 10 万次校验;上线前务必压测 - 真正关键的防护点不在触发器里:撤掉应用账号的直接
UPDATE权限,只开放带校验的存储过程,再配合 MySQL 8.0+ 的列级权限(GRANT UPDATE(salary) ON ...)
最常被低估的一点:触发器无法替代权限模型。它不防人,只防数;一旦权限配置松动、定义者账户被滥用、或应用绕过函数直连表,整套防护就形同虚设。


















