mysql行级权限需用sql security definer视图+存储函数封装用户映射逻辑,并严格收回基表权限;直接where中用子查询或user()过滤不可靠,易因多行、null、类型转换或连接池导致越权或空结果。

直接在 WHERE 子句里写 user_id = (SELECT ...) 会报错,除非你用 EXISTS、IN 或聚合函数包装;真正能落地的行级权限控制,得靠显式关联 + 避开 NULL 陷阱 + 控制递归深度。
为什么 WHERE user_id = (SELECT user_id FROM roles WHERE ...) 会失败
子查询返回多行时,SQL 标准不允许直接用于等值比较。哪怕用户只属于一个角色,只要 roles 表里有重复或历史残留数据,就可能触发 Subquery returns more than 1 row 错误。
-
IN看似简单,但遇到NULL值时整条记录会被静默过滤(因为col IN (1, 2, NULL)判定为UNKNOWN) -
= ANY(...)虽然语法合法,但可读性差,且多数 ORM 不支持参数化嵌套 - 硬编码用户 ID(如
WHERE user_id = 123)完全不可维护,上线即失效
用 EXISTS 实现安全、可下推的行级过滤
EXISTS 不取数据、只判存在,天然规避 NULL 和多行问题,还能被优化器下推到基表扫描层——只要子查询里正确关联主表字段。
- 必须把主表别名带进子查询,例如
rp.resource_id = r.id,否则变成无关联子查询,结果不可控 - 子查询内写
SELECT 1,不是SELECT *或SELECT id,避免列解析开销 - 参数要用占位符(如
?或:user_id),别拼字符串,防止 SQL 注入和执行计划缓存污染 - 示例:
SELECT * FROM orders o
WHERE EXISTS (
SELECT 1
FROM user_roles ur
JOIN role_permissions rp ON ur.role_id = rp.role_id
WHERE ur.user_id = ?
AND rp.permission_code = 'view_order'
AND rp.order_id = o.id
);
处理角色继承:必须用 WITH RECURSIVE 展开权限链
如果“管理员”角色继承了“编辑”角色的权限,仅两层 JOIN 无法覆盖全部路径。这时候递归 CTE 不是可选项,是必选项。
- 非递归部分(
UNION前)必须存在,且只能查直接分配的角色 - 递归部分(
UNION后)要确保连接条件指向父角色,比如ri.parent_id = ur.role_id - PostgreSQL 默认递归深度为 100,若组织架构超深,得加
SEARCH DEPTH FIRST BY role_id SET ordercol并配WHERE ordercol - 最后务必加
DISTINCT,否则继承树分叉会导致同一资源被多次匹配
容易被忽略的性能与权限绕过点
很多人调通逻辑就以为完事了,但线上一压就崩——问题往往出在索引和权限隔离上。
-
user_roles和role_permissions表必须在关联字段建联合索引,例如(user_id, role_id)和(role_id, permission_code, resource_id),否则EXISTS变全表扫描 - 视图或查询入口层没收回底层表的
SELECT权限,用户绕过视图直查基表,权限形同虚设 - MySQL 8.0+ 对
IN (subquery)有 5MB 结果集限制,而EXISTS没这限制,高权限用户场景优先选EXISTS - PostgreSQL 的
LATERAL子查询能简化写法,但 MySQL 5.7 不支持,跨库迁移前得先验语法兼容性
最麻烦的不是写对语法,而是让每个角色继承路径都收敛、每张权限表都有对应索引、每次查询都不依赖未授权的基表访问——这些细节不抠,权限系统上线后就是个定时漏洞。










