EXISTS比IN更可靠,因IN遇NULL会判为UNKNOWN导致静默越权;必须显式关联外层字段并用SELECT 1,多层角色继承需先用WITH RECURSIVE展开权限链。

WHERE里用EXISTS检查权限比IN更可靠
直接在主查询的WHERE中嵌套权限判断,首选EXISTS而非IN。因为IN遇到子查询结果含NULL时,整行条件会判为UNKNOWN,查不到数据却无报错,极易引发静默越权。
典型写法必须显式关联外层字段,并用SELECT 1:
SELECT * FROM resource r
WHERE EXISTS (
SELECT 1
FROM role_user ru
JOIN role_permission rp ON ru.role_id = rp.role_id
WHERE ru.user_id = ?
AND rp.permission_code = 'edit_article'
AND rp.resource_id = r.id
);-
ru.user_id = ?中的?是应用传入的用户ID,别硬编码或依赖current_user() -
rp.resource_id = r.id这行不能漏——没它就变成非相关子查询,逻辑全错 - 子查询里写
SELECT *或SELECT 'x'都可能干扰优化器,SELECT 1最稳妥 - 若
role_user.user_id或role_permission.resource_id没索引,这个查询会变慢,不是语法问题,是执行计划问题
多层角色继承必须用WITH RECURSIVE预展开
当系统支持“角色A继承角色B,B又继承C”,单纯两层JOIN无法覆盖全部权限路径。这时候EXISTS子查询没法递归,必须先用WITH RECURSIVE算出用户最终拥有的所有角色ID集合。
PostgreSQL示例(假设继承关系存于role_inherit(parent_id, child_id)):
WITH RECURSIVE user_roles AS ( SELECT role_id FROM role_user WHERE user_id = 123 UNION SELECT ri.child_id FROM role_inherit ri INNER JOIN user_roles ur ON ri.parent_id = ur.role_id ) SELECT DISTINCT r.* FROM resource r JOIN role_permission rp ON rp.resource_id = r.id JOIN user_roles ur ON ur.role_id = rp.role_id WHERE rp.permission_code = 'delete_comment';
- 递归CTE必须有非递归部分(第一行
SELECT)和递归部分(UNION后),缺一不可 - PostgreSQL默认递归深度100,长链需加
SEARCH DEPTH FIRST BY role_id SET ordercol并配合WHERE ordercol <= 200 - 必须加
DISTINCT——继承可能导致同一资源被多次匹配 - 别把
WITH塞进EXISTS里,优化器可能放弃索引;先物化再JOIN才是正解
子查询返回列类型必须和外层严格一致
如果外层user_id是BIGINT,子查询却返回VARCHAR(比如雪花ID或用户名),数据库会隐式转换整列,导致索引失效、全表扫描。MySQL在EXPLAIN里只显示type: ALL,不报错也不警告。
- 正确写法:
user_id IN (SELECT id FROM allowed_users WHERE role = 'sales'),确保id类型与外层user_id一致 - 错误写法:
user_id IN (SELECT username FROM allowed_users),即使username内容看起来像数字也不行 - 权限表用字符串ID时,子查询也得返回
VARCHAR,别用CAST(id AS CHAR)临时补救——建表时就该统一 - 别在子查询里用
SELECT *或SELECT 1以外的常量(如SELECT 'x'),优化器可能误判为非确定性表达式
视图里不能直接参数化,得靠函数或会话变量包装
标准SQL视图不接受参数,所以CREATE VIEW user_resources AS SELECT * FROM resource WHERE user_id = ?会直接报错syntax error at or near "?"。真正可行的是用数据库运行时能识别的上下文函数,但各库行为差异大。
- PostgreSQL推荐用
current_setting('app.user_id', true)::int,前提是应用连接后执行SET app.user_id = 123 - SQL Server优先用
ORIGINAL_LOGIN()而不是SUSER_SNAME(),因前者锁定连接初始身份,防EXECUTE AS绕过 - MySQL 8.0+无法在视图里安全解析
USER()返回的user@host格式,实际做法是应用层SET @current_user_id = 123,视图里用@current_user_id - 无论哪种方式,都必须收回底层表的
SELECT权限,只授予视图权限,否则用户可绕过视图直查原表
复杂点在于:权限逻辑一旦涉及递归、类型转换、会话上下文,就不再是纯SQL语法问题,而是数据库配置、应用层协作、执行计划三者咬合的结果。最容易被忽略的是EXISTS里漏掉外层字段关联,以及递归CTE没加循环检测——这两处不出错,但结果永远不对。

















