postgresql触发器无法直接识别应用层“特定角色”,必须由应用显式执行set app.dml_role='xxx',触发器内用current_setting('app.dml_role', true)获取并校验,未设置则raise exception拒绝;仅before触发器配合return null(行级)或不return(语句级)可可靠拦截,且不可替代rls或权限回收等前置控制。

PostgreSQL 触发器无法直接识别调用角色是否为“特定角色”
触发器函数里调用 CURRENT_ROLE 或 SESSION_USER,返回的是当前会话的**活跃角色**(即 SET ROLE 后的角色),但这个值在触发器执行时可能已被重置或不可靠;更重要的是,CURRENT_ROLE 在未显式 SET ROLE 时等于 SESSION_USER,而后者是登录用户,不是应用层意图代表的操作角色。你真正想拦的“特定角色”,比如 'reporter' 或 'etl_user',必须由应用在会话中主动设置并传递上下文。
- 应用需在执行 DML 前运行
SET app.dml_role = 'reporter'(推荐用带命名空间的自定义参数) - 触发器内用
current_setting('app.dml_role', true)获取,第二个参数true避免未设时报错 - 不要依赖
CURRENT_USER()—— 它返回连接账号(如app_pool@10.0.2.5),和业务角色完全无关 - 如果应用没设该变量,
current_setting返回NULL,此时应拒绝操作(不能 fallback 到默认角色,那会绕过控制)
BEFORE 触发器 + RAISE EXCEPTION 是唯一可靠拦截方式
PostgreSQL 中,只有 BEFORE 触发器能在语句执行前中断事务;AFTER 触发器即使 RAISE EXCEPTION,数据也已写入,无法回滚(除非手动 ROLLBACK,但触发器做不到)。且注意:RAISE EXCEPTION 本身不足以阻止 DELETE/UPDATE —— 必须配合 RETURN NULL(对行级触发器)或不 RETURN(对语句级触发器)。
- 对行级触发器(
FOR EACH ROW):在BEFORE中检查后,若要拒绝,写RETURN NULL;否则必须RETURN NEW或RETURN OLD - 对语句级触发器(
FOR EACH STATEMENT):不RETURN即可终止,但无法访问NEW/OLD,适合做粗粒度拦截(如全表禁止) - 错误信息里建议包含角色名和操作类型,例如:
RAISE EXCEPTION 'DML blocked: role % attempted % on %', current_setting('app.dml_role', true), TG_OP, TG_TABLE_NAME
别用触发器替代 RLS 或权限回收——它只是补丁
触发器是最后一道防线,不是权限模型。如果你发现需要靠触发器来“阻止特定角色的 DML”,说明前置控制已经失守:要么没回收基表的 INSERT/UPDATE/DELETE 权限,要么没启用 RLS。PostgreSQL 的标准路径是:
- 先
REVOKE INSERT, UPDATE, DELETE ON TABLE mytable FROM reporter,彻底切断权限链 - 再通过
SECURITY DEFINER函数暴露受控接口(如insert_safe_record()),函数内部校验current_setting('app.dml_role')并写审计日志 - 最后加触发器作为兜底(比如防止 DBA 直连绕过函数),而非主控手段
- RLS 策略(
USING/WITH CHECK)对SELECT也生效,而触发器对SELECT完全无效
event trigger 不能用于拦截普通 DML,只管 DDL 和系统事件
有人误以为 CREATE EVENT TRIGGER ... ON ddl_command_start 能拦 UPDATE,这是错的。ddl_command_start 只响应 CREATE/DROP/ALTER 等 DDL,不响应任何 DML(INSERT/UPDATE/DELETE)。DML 的拦截只能靠常规触发器(CREATE TRIGGER),且仅限于目标表。
- 想拦所有
DROP TABLE?用EVENT TRIGGER+ddl_command_start+WHEN TAG IN ('DROP TABLE') - 想拦某个用户对
users表的UPDATE?只能在users上建BEFORE UPDATE行级触发器 - 跨表通用拦截?不存在。PostgreSQL 不提供“全局 DML 拦截钩子”,这是设计使然,不是缺陷
SET app.dml_role,整个防护就形同虚设。










