mysql中脱敏必须用before insert触发器修改new值,after中new只读;推荐left+right+concat定位截取并校验长度、null及字符集,多敏感字段宜用视图+权限隔离而非臃肿触发器。

触发器里不能直接改 NEW 值,得用 BEFORE INSERT
MySQL 和 PostgreSQL 的触发器中,NEW 在 BEFORE INSERT 阶段是可写的,但 AFTER INSERT 里只读——想脱敏再入库,必须选 BEFORE INSERT。否则会报错:Can't update table 'xxx' in stored function/trigger(尤其在 MySQL 中)。
常见错误是写成 AFTER 触发器,然后试图赋值 NEW.phone := mask_phone(NEW.phone),这在 PostgreSQL 里语法合法但实际无效,在 MySQL 里直接报错。
- MySQL:只支持
BEFORE INSERT/UPDATE修改NEW - PostgreSQL:支持
BEFORE行级触发器返回修改后的NEW记录 - SQL Server:用
INSTEAD OF触发器,本质是拦截并重写插入逻辑
脱敏函数要自己写,别依赖触发器外的业务层
触发器内没法调用应用层的脱敏工具(比如 Java 的 DesensitizationUtils.maskMobile()),所有逻辑必须内置。推荐用数据库原生函数拼接 + 截取,简单可靠。
例如手机号脱敏(保留前3后4):
-- MySQL 示例 DELIMITER $$ CREATE TRIGGER user_before_insert BEFORE INSERT ON users FOR EACH ROW BEGIN SET NEW.phone = CONCAT(LEFT(NEW.phone, 3), '****', RIGHT(NEW.phone, 4)); END$$ DELIMITER ;
注意点:
-
LEFT()/RIGHT()在字段为空或长度不足时会返回NULL,建议加IF(LENGTH(NEW.phone) = 11, ..., NEW.phone)校验 - PostgreSQL 要用
SUBSTRING(NEW.phone FROM 1 FOR 3)和||拼接 - 避免在触发器里调用存储过程做复杂脱敏(如 AES 加密),性能差且难调试
敏感字段多时,触发器容易失控,优先考虑视图+列权限
如果一张表有 id_card、email、address 多个需脱敏字段,全塞进一个触发器会让逻辑臃肿、难维护。更稳妥的做法是:原始字段存明文,另建脱敏视图暴露加工后字段,并配 GRANT SELECT ON view_users_masked TO app_user。
这样做的好处:
- 脱敏逻辑集中、可测试(直接查视图验证结果)
- 不影响原始数据完整性,审计或 ETL 仍可读明文
- 避免触发器嵌套、递归或事务失败导致写入阻塞
- 权限控制粒度更细(比如 DBA 可查原表,应用只查视图)
触发器适合“强制写入即脱敏”的硬性合规场景(如 GDPR 要求日志表不得存原始手机号),但不是通用解法。
MySQL 8.0+ 支持生成列,比触发器更轻量
如果只是做固定格式脱敏(如邮箱掩码为 u***@d***.com),MySQL 8.0 的 GENERATED COLUMN 是更优选择——不占存储、不走触发器开销、自动更新。
示例:
ALTER TABLE users ADD COLUMN email_masked VARCHAR(255) AS (CONCAT(LEFT(email, 1), '***@', SUBSTRING_INDEX(SUBSTRING_INDEX(email, '@', -1), '.', 1), '***.', SUBSTRING_INDEX(email, '.', -1))) STORED;
注意:
-
STORED列会物理存储,VIRTUAL不存但每次查询计算——选哪个看读写比 - 生成列表达式不能含子查询、函数(如
NOW())、变量,但LEFT/CONCAT等标量函数可以 - PostgreSQL 用
GENERATED ALWAYS AS,语法类似,但暂不支持索引(截至 16.x)
真正麻烦的是需要条件判断的脱敏(比如按用户角色决定是否脱敏),这种还是得回触发器或应用层处理——数据库没那么灵活。











