应通过current_setting('app.tenant_id', true)获取租户ID并校验非空,INSERT/UPDATE时用IS DISTINCT FROM比对NEW.tenant_id与会话值,DELETE时同理校验OLD.tenant_id,且需确保连接池为会话池模式。

触发器里怎么拿到当前租户ID
PostgreSQL 触发器函数本身不自动携带租户上下文,必须依赖外部传入。最可靠的方式是通过 current_setting('app.tenant_id') 读取会话级配置——前提是应用在每次连接后都执行了 SET app.tenant_id = 't123'。
如果没设,current_setting() 会抛出异常,所以必须加 missing_ok := true 参数并做空值判断:
DECLARE
tenant_id TEXT := current_setting('app.tenant_id', true);
BEGIN
IF tenant_id IS NULL THEN
RAISE EXCEPTION 'tenant_id not set in session';
END IF;
-- 后续校验逻辑
END;别用 SESSION_USER 或 CURRENT_USER 替代,它们是数据库角色,和业务租户无关;也别依赖 JWT 解析或函数参数传入,那会让触发器失去通用性。
INSERT/UPDATE 时如何强制校验 tenant_id 字段
核心是比对新行的 tenant_id 字段是否等于会话中设置的值。注意:字段名必须统一(比如所有租户表都叫 tenant_id),且类型要一致(推荐 TEXT 或 UUID)。
常见错误是忽略 NULL 场景或类型隐式转换失败:
- 如果字段允许为
NULL,而会话里有值,应明确拒绝:IF NEW.tenant_id IS DISTINCT FROM tenant_id THEN ... - 避免写成
NEW.tenant_id != tenant_id,因为当任一为NULL时结果为NULL,条件不成立 - 若字段是
UUID类型,而current_setting()返回TEXT,需显式转换:NEW.tenant_id != tenant_id::UUID
示例片段:
IF NEW.tenant_id IS DISTINCT FROM tenant_id THEN
RAISE EXCEPTION 'tenant_id mismatch: expected %, got %',
tenant_id, NEW.tenant_id;
END IF;DELETE 操作要不要加租户校验
要,而且更关键。否则攻击者可能伪造 WHERE id = 123 删除其他租户数据——只要没带 AND tenant_id = 't123',就可能越权。
触发器无法直接拦截 WHERE 条件,但可以在 BEFORE DELETE 中检查 OLD.tenant_id 是否匹配:
-
OLD.tenant_id总是存在的(因为是已存在的行),所以不用判空 - 必须用
IS DISTINCT FROM,防止OLD.tenant_id是NULL导致校验失效 - 不要只校验
NEW(DELETE 没有NEW),也不要试图重写WHERE(触发器做不到)
一旦不匹配,直接 RAISE EXCEPTION,事务回滚,DELETE 失败。
性能与部署注意事项
每个触发器都会带来额外开销,尤其高频写入表。实际影响取决于校验逻辑复杂度——纯字符串比对几乎无感,但若在里面查表或调函数就会拖慢。
容易被忽略的点:
- 触发器必须定义在每个需要隔离的表上,不能“继承”或“全局启用”
- 迁移脚本里要确保先
CREATE FUNCTION,再CREATE TRIGGER,顺序错会导致创建失败 - 应用连接池(如 PgBouncer)若用事务池模式,
SET语句不会跨事务保留,必须改用会话池模式,或在每次查询前重新SET - 测试时务必用真实连接复现,别只在 psql 里手动 SET 后跑,那掩盖了连接池问题
租户ID 校验本身很简单,难的是让整个链路——从连接建立、SET 执行、到触发器运行——始终处于受控状态。少一个环节,隔离就形同虚设。

















