触发器不能替代权限控制或租户上下文绑定,仅是最后一道校验防线;必须通过会话变量动态获取tenant_id,严禁硬编码,并在连接池初始化时重置变量以防串租户。

触发器不能替代权限控制,也不能弥补应用层缺失的租户上下文绑定——它只是最后一道校验防线。直接在触发器里写死 tenant_id 值等于放弃隔离。
BEFORE INSERT 中如何安全填充 tenant_id
必须依赖会话变量或配置项动态获取,不能硬编码。不同数据库语法差异大,且都要求应用层提前设置:
- MySQL:用
@tenant_id用户变量,需在每次连接或事务开始前执行SET @tenant_id = 't_123';触发器内写SET NEW.tenant_id = IFNULL(@tenant_id, 'invalid'); - PostgreSQL:用
current_setting('app.tenant_id'),需配合SET LOCAL app.tenant_id = 't_123';触发器函数中赋值为NEW.tenant_id := current_setting('app.tenant_id', true)::text; - SQL Server:不支持修改插入行,只能用
INSTEAD OF INSERT,逻辑更重,易出错 - 所有场景下都必须加非空校验,否则
NULL写入可能违反NOT NULL约束或导致索引失效
BEFORE UPDATE 为什么必须拦截 tenant_id 修改
用户或 DBA 可能绕过应用直连数据库执行 UPDATE orders SET tenant_id = 't_456' WHERE id = 123,仅靠 INSERT 触发器完全无法防御。
- 不能简单地覆盖
NEW.tenant_id,否则业务合法更新其他字段(如status)时,tenant_id会被意外重置 - 正确做法是显式比对:
IF NEW.tenant_id != OLD.tenant_id THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'tenant_id cannot be modified'; END IF; - 注意 MySQL 的
SIGNAL仅在 5.5+ 支持,低版本得用INSERT INTO nonexistent_table VALUES()触发错误
连接池环境下 tenant_id 变量为何会“串租户”
连接复用时,上一个请求设置的 @tenant_id 或 current_setting 若未重置,下一个租户的查询就会沿用旧值——这是最隐蔽也最常被忽略的问题。
- HikariCP 等主流连接池支持
connection-init-sql,应设为SET @tenant_id = NULL(MySQL)或RESET app.tenant_id(PostgreSQL) - 若用 Spring,还需确保
@Transactional方法结束时主动清理,不能只靠连接归还 - 日志中出现“本该查租户 A 却返回租户 B 的数据”,八成是这个原因
触发器里的租户校验永远只是“补漏”,不是“兜底”。真正可靠的隔离必须从连接初始化、SQL 生成、权限分配三层同时落地——漏掉任何一层,tenant_id 就只是个摆设字段。










