安全拼接SQL需白名单校验权限字段、QUOTE()包裹值、人工核对@sql、会话变量传租户ID并校验、显式SIGNAL报错、禁止绑定列名/表名。

MySQL存储过程中怎么安全拼接SQL做动态权限检查
不能直接用 CONCAT 拼接用户输入进 EXECUTE,否则就是SQL注入温床。真实业务里,权限字段(比如 tenant_id、dept_code)往往来自调用方参数或会话变量,必须先白名单校验再参与拼接。
- 只允许从预定义的列名列表中选择权限字段,比如
['user_id', 'org_id', 'project_id'],用CASE或查找表比对,不接受任意字符串 - 值部分必须用
QUOTE()包裹,且后续在EXECUTE前用SELECT @sql打印出来人工核对一次(上线前可删,但开发期必做) - 避免在拼接SQL里出现子查询或JOIN——动态权限逻辑应尽量扁平,复杂判断提到应用层或用视图封装
如何让存储过程读取当前会话的业务上下文(如租户ID)
MySQL没有类似Oracle的 APP_CONTEXT,也不能直接读取HTTP Header。常见做法是靠客户端显式传参 + 会话变量双重保障。
- 调用时必须设置
SET @current_tenant_id = 123;,并在存储过程开头用IF @current_tenant_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Missing tenant context'; END IF; - 不要依赖
USER()或CURRENT_USER()推导业务身份——数据库账号和业务用户完全无关 - 如果用连接池(如Druid),确保每次获取连接后都重置会话变量,避免上一个请求的
@xxx泄露到下一个
动态权限校验失败时,该抛错还是静默过滤
取决于调用方是否需要感知越权。生产环境强烈建议显式报错,而不是返回空结果集——后者会让前端误以为“没数据”,排查时根本想不到是权限问题。
- 统一用
SIGNAL SQLSTATE '45000'抛出自定义错误,消息里带关键信息,比如CONCAT('Access denied for tenant ', @current_tenant_id) - 不要用
SELECT ... INTO后判断结果为空来代替权限检查——这属于事后补救,绕过了权限控制点 - 若确实要静默(如统计类查询),必须在注释里写明“此处跳过租户隔离”,并经安全评审签字
为什么不能在存储过程里用 PREPARE 绑定权限参数
因为 PREPARE 的参数只能是标量值,无法绑定列名或表名。你想实现 “按不同字段查权限”,比如 WHERE dept_id = ? 或 WHERE region_code = ?,就必须拼接列名——而这正是最危险的环节。
-
PREPARE stmt FROM 'SELECT * FROM orders WHERE ? = ?'; EXECUTE stmt USING @col_name, @val;—— 这种写法无效,?不能代入列名 - 真正能绑定的只有值,所以列名必须走白名单校验+拼接,表名同理;高危操作必须加日志,例如
INSERT INTO audit_log VALUES (NOW(), 'auth_check', @sql); - 如果发现多个地方重复做列名校验,说明该抽象成一个校验函数,比如
fn_validate_column_name(@input),返回合法列名或NULL
实际跑通的关键不在语法多漂亮,而在每一步动态拼接前,有没有人盯着那条 @sql 看一眼:它真的只包含预期的字段和值吗?这个动作没法自动化,也最容易被跳过。










