SQL触发器不能实现行级数据权限控制,因其不响应SELECT、JOIN、子查询等读操作,且无法在查询阶段注入过滤条件;正确方案是RLS、会话感知视图或参数化存储过程。

不能用DML触发器实现行级权限动态控制——它根本拦不住SELECT,也拦不住JOIN和子查询,强行用等于留后门。
为什么BEFORE INSERT/UPDATE触发器拦不住越权读
用户执行 SELECT * FROM orders WHERE user_id = 123,触发器完全不响应。标准SQL没有 BEFORE SELECT 这种东西。哪怕你在 UPDATE 触发器里加了校验,用户仍可直接查原表、用 JOIN users ON orders.owner_id = users.id 拿到他人数据,或通过 UNION ALL 拼接多张表绕过单表校验。
常见错误现象包括:应用报 ERROR: permission denied,但日志显示是触发器 RAISE EXCEPTION 抛的,实际用户早已从视图或临时表把全量数据导出。
- 触发器只在DML事件发生时运行,对查询类操作零干预
- RLS(行级安全策略)是在查询计划生成阶段注入
WHERE条件,能走索引、可下推;触发器做不到 - PostgreSQL 的
CURRENT_USER是角色名,不是业务用户ID;MySQL 的USER()返回app@10.2.3.4,无法直接映射到员工表
如果非要用触发器做写操作校验,必须显式传参
靠数据库内置函数识别当前业务用户几乎必然失败。正确做法是应用层主动注入标识,再由触发器读取:
- PostgreSQL:应用执行
SET LOCAL app.current_user_id = 'u123',触发器用current_setting('app.current_user_id', true) - MySQL:应用执行
SET @current_user_id = 'u123',触发器用@current_user_id - SQL Server:应用执行
SET CONTEXT_INFO 0x75313233(u123的十六进制),触发器用CONVERT(VARCHAR(128), CONTEXT_INFO())
否则你看到的 CURRENT_USER 很可能是 app_rw 或 dbo,所有校验形同虚设。
INSERT/UPDATE/DELETE中校验逻辑怎么写才不翻车
核心不是“查权限”,而是“比归属字段”。假设订单表有 owner_id 字段,规则是“用户只能操作自己名下的订单”:
-
INSERT:只检查NEW.owner_id = 当前用户ID -
UPDATE:必须同时检查OLD.owner_id = 当前用户ID(保证原属自己)且NEW.owner_id未被篡改(若业务不允许转交,就禁止改该字段) -
DELETE:只检查OLD.owner_id = 当前用户ID
容易踩的坑:UPDATE 触发器里只校验 NEW.owner_id,结果用户把别人订单的 status 改成“已发货”,却没被拦住——因为 owner_id 没变,但业务上这属于越权操作。
真正该用的方案:RLS、会话感知视图、参数化存储过程
行级权限必须在查询入口处闭环,而不是等数据写完再补救:
- PostgreSQL / SQL Server 2016+ / Oracle 12c+:用
CREATE POLICY开启RLS,表达式调用current_setting('app.user_id')或ORIGINAL_LOGIN() - MySQL 8.0+:建
SQL SECURITY INVOKER视图,用SUBSTRING_INDEX(USER(), '@', 1)提取用户名,再关联业务表;关键要REVOKE SELECT ON base_table FROM user - 兜底方案:所有查询走带参数的存储过程,严格校验输入并拼
WHERE owner_id = ?,禁用动态SQL裸拼
最容易被忽略的是权限授予链是否断裂:哪怕RLS策略写得再严,只要忘了 REVOKE SELECT ON orders FROM public,用户就能绕过策略直连原表——触发器从来管不了这个。

















