存储过程动态权限校验必须用白名单硬编码字段名或由应用层预处理合法字段,严禁拼接列名;会话变量传租户上下文需强制校验非空并重置;权限失败须 SIGNAL 显式报错,复杂逻辑应交由应用层处理。

存储过程里不能直接拼接列名做权限字段切换
想让一个存储过程支持按 tenant_id、dept_code 或 project_id 动态校验,最常见错误是写成:CONCAT('WHERE ', @field, ' = ', QUOTE(@value))。这看似安全,但只要 @field 来自不可信输入(比如 HTTP 参数解析后塞进会话变量),就等于把 SQL 注入的入口亲手打开。
MySQL 的 PAREPARE 语句只允许绑定值,不支持绑定列名或表名。所以你没法用 EXECUTE stmt USING @col_name, @val 这种方式绕过——它根本不会生效,@col_name 会被当作文本字面量处理,导致语法错误或逻辑失效。
真正可行的做法只有两个:
- 用白名单硬编码字段名:比如只允许
@field取值为'tenant_id'、'dept_code'、'project_id',通过CASE WHEN @field = 'tenant_id' THEN ...分支拼接,其他值一律SIGNAL报错 - 把字段选择逻辑提到应用层:存储过程只接收已确认合法的字段名(如枚举值 1/2/3),内部用
IF映射到对应列,避免任何字符串拼接参与 SQL 构建
会话变量传租户上下文必须显式校验且重置
MySQL 没有业务上下文感知能力,CURRENT_USER() 返回的是数据库账号,不是登录系统的用户。所以靠 SET @current_tenant_id = ? 传参是主流做法,但极易出问题:
- 调用前忘记
SET,过程内读到NULL却没检查,导致 WHERE 条件恒真(全库扫描)或恒假(空结果误判为无数据) - 连接池复用连接时,上一个请求设的
@current_tenant_id残留,被下一个租户意外继承 - 存储过程中没用
IF @current_tenant_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Missing tenant context'; END IF;做兜底
正确姿势是在每个存储过程开头强制校验,并在应用层每次从连接池取连接后执行 RESET SESSION 或至少 SET @current_tenant_id = NULL。
动态权限失败必须显式报错,不能静默过滤
返回空结果集是最危险的“静默失败”。前端看到 [],第一反应是“没数据”,而不是“我越权了”,排查时完全想不到查权限日志或审计 SQL。
生产环境必须用 SIGNAL SQLSTATE '45000' 中断执行,消息里带关键上下文:
CONCAT('Access denied: tenant ', @current_tenant_id, ' not allowed on resource ', @resource_id)- 错误码统一,方便网关或中间件识别并转译为 403
- 避免用
SELECT ... INTO后判断变量是否为空来“模拟”权限检查——这是绕过控制点,审计时直接算漏洞
复杂权限逻辑别硬塞进存储过程
如果权限规则涉及多级部门继承、角色组合、时间窗口、审批流状态等,硬在存储过程里用 JOIN 和子查询实现,会导致:
- SQL 可读性归零,后续没人敢改
- 执行计划难以优化,容易触发全表扫描
- 无法利用应用层缓存(比如部门树、角色权限快照)
- 和 MySQL 权限表(
mysql.db、mysql.tables_priv)完全脱节,变成两套权限体系
建议拆分:存储过程只做最基础的租户/组织隔离(单字段等值匹配),复杂判断交给应用层调用权限服务,或提前物化到视图/临时表中。毕竟 MySQL 不是规则引擎,它的强项是数据存取,不是策略计算。


















