唯一能拦住越权改薪资的做法是用before update触发器+signal报错,并依赖应用层设置的会话变量(如@current_app_user)识别真实操作人,因current_user()和user()仅反映数据库连接身份、无法标识业务用户,且表权限与check约束无法校验操作主体与上下文。

唯一能拦住越权改薪资的做法,是用 BEFORE UPDATE 触发器 + SIGNAL 主动报错,且必须靠会话变量识别真实操作人——CURRENT_USER() 和 USER() 都不可信。
为什么不能只靠表权限或 CHECK 约束
表级 UPDATE 权限一旦授予,用户就能执行任意合法 SQL;CHECK 只能限制单行字段范围(比如 salary BETWEEN 3000 AND 50000),但拦不住 HR 以外的人把张三的薪资从 15000 改成 15001。真正要防的是“谁在什么时候改了谁的薪资”,这必须依赖运行时上下文判断。
-
CURRENT_USER()返回的是数据库连接账号(如app@10.0.2.5),不是调用接口的员工 ID -
USER()同样不可靠,它返回username@host,DBA 直连或 ETL 工具导入时根本不会变 - 别写
IF USER() NOT LIKE 'hr@%'这类逻辑——测试环境可能全放行,生产环境一换账号就失效
怎么让触发器知道“谁”在改薪资
应用层必须提前设置会话变量,触发器再读取。MySQL 推荐用 @current_app_user,PostgreSQL 用 SESSION_CONTEXT('userid')。
- 应用执行更新前,先跑一句:
SET @current_app_user = 'u78901'; - 触发器里用
COALESCE(@current_app_user, CURRENT_USER())fallback,避免变量未设导致 NULL - SQL Server 要用
CONTEXT_INFO(),且必须SET CONTEXT_INFO二进制值,不能直接赋字符串 - 如果应用没设变量,
@current_app_user是 NULL,COALESCE会退到CURRENT_USER(),但此时你已丢失真实身份——这点容易被忽略
BEFORE UPDATE 中怎么写拦截逻辑
重点不是“能不能改”,而是“谁在什么条件下能改”。校验必须同时覆盖操作人、字段变更、业务状态三者。
- 只比对
OLD.salary != NEW.salary不够:NULL 比较结果为 UNKNOWN,得写成(OLD.salary != NEW.salary) OR (OLD.salary IS NULL) != (NEW.salary IS NULL) - 允许 HR 改所有人的薪资,但财务只允许改自己部门的:
IF @current_app_user NOT IN ('hr_admin', 'finance_lead') AND NEW.department != OLD.department THEN ... - 状态为
'archived'的员工薪资冻结:IF OLD.status = 'archived' AND OLD.salary != NEW.salary THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '归档员工薪资不可修改'; - 别在触发器里查
SELECT role FROM users WHERE id = @current_app_user——高并发下查表会拖慢整个事务,角色信息应由应用传入
为什么 AFTER UPDATE 或“静默还原”都不可靠
AFTER UPDATE 已经改完了,再拦没意义;“改回去”看似安全,实则埋雷。
- 触发器里执行
UPDATE employee SET salary = OLD.salary会再次触发自身,MySQL 默认禁止递归,SQL Server 可能死循环 - 并发场景下,A 用户刚改完,B 用户的触发器还原操作可能覆盖 A 的合法更新
- 事务回滚由外部控制,触发器无法保证还原操作和主更新原子性一致
-
SIGNAL SQLSTATE '45000'是唯一明确中止、不落盘、错误可捕获的方式,MySQL 5.5+ 全支持
最易被忽略的一点:触发器继承定义者的权限,不是调用者权限。如果你用 root 创建触发器,它就能以 root 身份执行任何语句——哪怕低权限用户触发,也可能越权写日志表或调用系统函数。定义触发器前,务必用最小权限账号创建。










