AI生成的SQL如何确保不同用户只能看到自己权限范围内的数据?
语义解析 · 权限注入 · 执行校验 · 审计回写 —— 一条贯穿全链路的工程主线
摘要
自然语言转SQL(NL2SQL)让业务人员用一句话就能取数,但AI生成的SQL天然是"权限盲"的:模型只关心语义是否对齐、语法是否正确,不关心执行者是谁、能看到哪些行。一旦把这类SQL直接打到数仓上,行级权限(Row-Level Security, RLS)就从"数据库配置项"变成了"生成链路中的必答题"。本文提出一条贯穿全文的分析主线——"语义解析 → 权限注入 → 执行校验 → 审计回写"的四层闭环,把行级权限从被动的事后拦截,前移为生成过程中的结构化约束。文章依次讨论:NL2SQL的语义解析与Schema链接为何会"绕过"权限、权限注入的三种工程路径(提示词约束、AST改写、数据库原生RLS)、多租户与列级权限的叠加、执行期的越权检测与结果集校验、审计回写如何反哺模型,以及面向Agentic BI的前沿预判。全文给出可落地的步骤、伪代码与评测口径,并对经典方法逐一点评,力求在理论深度与工程可操作性之间取得平衡。
目录
一、问题的本质:AI生成的SQL为什么天生"权限盲"
先把问题说透。NL2SQL系统的输入是一句自然语言和一份数据库Schema,输出是一条可执行的SQL。整个过程中,模型优化的目标是"语义等价"——生成的SQL执行结果要能回答用户的问题。至于"这个用户有没有资格看这些行",在标准的NL2SQL任务定义里根本不存在。Spider、BIRD等主流基准测试(Yu等,2018;Li等,2023)评测的是执行准确率(Execution Accuracy),数据集里没有"用户身份"这一维度,自然也不会考察权限。笔者认为,这不是模型的缺陷,而是任务定义的盲区:权限是执行上下文(context)的属性,而NL2SQL把上下文压缩掉了。
盲区带来的风险是具体的。设想一个销售分析场景:华东区销售只能看本区订单,华南区销售只能看华南区。用户问"上个月哪个产品线卖得最好",模型生成的SQL是 SELECT product_line, SUM(amount) FROM orders WHERE month='2024-05' GROUP BY product_line。这条SQL语法正确、语义正确,但它把全国数据都聚合了。如果直接执行,华东区销售就看到了华南区的业绩——一次典型的行级越权。更隐蔽的情况是:模型"好心"地根据用户提问里的"我们区"推断出 WHERE region='华东',看起来对了,但这是模型猜的,不是权限系统判定的,用户换个说法("我们大区""我负责的片区")就可能猜错,甚至被诱导写出 WHERE 1=1。
这里要引入一个关键区分:"模型理解对了"和"权限约束住了"是两件事。前者是概率性的、可被提示词攻击绕过的;后者必须是确定性的、由系统强制执行的。把行级权限寄托在模型"自觉"上,等于把安全边界交给了一个不可解释的黑箱。本文的全部讨论,都建立在这个判断之上:权限必须是生成链路中的硬约束,而不是模型输出的软期望。
本文评述:业界常把NL2SQL的权限问题简单归为"加个WHERE条件",这低估了难度。真正的难点在于:WHERE条件加在哪一层、由谁加、加错了怎么发现、加了之后语义是否还成立。这四个问题对应本文的四层闭环,缺一层,防线就有缺口。
二、背景与概念坐标系:NL2SQL、RLS与权限模型
2.1 NL2SQL的技术演进与"上下文缺失"
NL2SQL从早期的序列到序列模型(Zhong等,2017)走到今天的LLM直接生成,能力提升显著。BIRD基准(Li等,2023)显示,GPT-4级别模型在带外部知识提示的条件下执行准确率可达较高水平,但即便最强模型在复杂Schema上仍有明显差距。工程上更常用的是"检索增强"路线:先用向量检索把相关表、列、示例(few-shot)召回,再拼进提示词让模型生成。这套做法在准确率上有效,但它恰恰暴露了上下文缺失问题——检索的是"知识上下文",不是"权限上下文"。用户身份、所属租户、数据域授权,这些信息默认不在提示词里。
近年来Schema链接(Schema Linking)研究(如Wang等,2020;Lei等,2020)致力于把自然语言中的实体对齐到表和列,这为权限注入提供了天然抓手:既然系统已经知道"订单金额"对应 orders.amount,那么它同样可以知道"这个用户能看哪些 orders 行"。本文评述:Schema链接的价值被低估了,它不只是提升准确率的手段,更是权限系统与语义理解之间的"公共坐标系"——权限策略可以挂在Schema元素上,语义解析的结果也能映射回同一套元素,两边才对得上。
2.2 行级权限(RLS)与列级权限(CLS)
行级权限指"同一张表,不同用户看到不同的行"。主流数据库都提供了原生支持:PostgreSQL的Row Security Policy(自9.5起)、Oracle的Virtual Private Database(VPD)、Snowflake的Row Access Policies、BigQuery的Row-Level Security、Databricks的Row Filter。列级权限则控制"哪些列可见",常见实现是视图、列掩码(masking)或动态数据脱敏。两者经常叠加:一个用户可能"能看本区订单,但看不到成本价列"。
需要强调的是,原生RLS是"最后一道防线",但它不是万能的。第一,它依赖数据库支持,异构数据源(MySQL、ClickHouse、Hive、API)能力参差;第二,它只管"行",不管"语义"——如果模型生成的SQL本身就把权限过滤写错了(比如把 region 拼成 region_code 导致谓词失效),RLS可能因为策略绑定不到正确列而失效;第三,它无法阻止"通过聚合/推断间接泄露"——比如用 COUNT(*) 反推他区数据量。本文评述:原生RLS是必要不充分条件,必须与生成期的权限注入、执行期的结果校验组合使用。
2.3 权限模型:RBAC、ABAC与策略即代码
RBAC(基于角色的访问控制)用角色聚合权限,简单但粒度粗;ABAC(基于属性的访问控制)用主体、资源、环境属性做判定,表达力强但策略复杂。工程上更实用的是"策略即代码"(Policy as Code),如Open Policy Agent(OPA)的Rego语言,把权限判定从应用代码里抽出来,做成可测试、可版本化的策略文件。对NL2SQL而言,OPA这类引擎的价值在于:它能在SQL生成后、执行前,对"这条SQL会访问哪些资源"做一次静态判定,把权限检查从"数据库执行时"提前到"SQL下发前"。这一点在第七节会展开。
三、分析主线:四层闭环的总体架构
本文的分析主线是一条闭环:语义解析(理解谁在问什么)→ 权限注入(把权限约束写进SQL或执行环境)→ 执行校验(确认没有越权并守住结果集)→ 审计回写(记录、告警、反哺策略与模型)。四层不是串行流水线,而是带反馈的环:审计层的越权事件会回流到解析层(更新用户画像)和注入层(收紧策略),形成持续收敛。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 语义解析层 │───▶│ 权限注入层 │───▶│ 执行校验层 │───▶│ 审计回写层 │
│ 意图/Schema │ │ 提示/AST/RLS │ │ 越权检测/守卫│ │ 日志/告警/反哺│
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
▲ │
└──────────────────── 策略与画像回流 ────────────────────┘
这条主线的独创性在于:它把权限从"数据库的一个配置项"重新定义为"生成链路的一个阶段"。传统做法是"先生成、再执行、执行时被RLS拦",属于事后防御;四层闭环把权限前移到解析和注入阶段,执行校验只做兜底。笔者认为,这个前移是必要的——因为越权一旦发生,即使被拦住,也已经产生了"模型试图越权"的信号,这个信号本身有价值(可能是提示词攻击,也可能是Schema理解偏差),应该被审计层捕获,而不是被数据库默默拦截后丢弃。
四、第一层 语义解析:把"谁在问"编码进理解过程
4.1 用户上下文的结构化建模
权限注入的前提是"知道用户是谁、能看什么"。这需要一份结构化的用户上下文,至少包含:用户ID、所属租户、角色列表、数据域授权(可访问的表/列/行范围)、敏感级别。工程上建议把它做成一个"权限上下文对象"(Permission Context),在请求入口处一次性组装,随请求贯穿全链路。伪代码如下:
class PermissionContext:
user_id: str
tenant_id: str
roles: list[str]
row_filters: list[RowFilter] # 如 region IN ('华东')
column_denies: set[str] # 如 {'cost_price'}
sensitivity_ceiling: int # 最高可访问敏感级
def build_context(token) -> PermissionContext:
claims = verify_jwt(token)
roles = fetch_roles(claims.sub)
filters = compile_policy(roles, claims.tenant) # 策略引擎编译
return PermissionContext(...)
这里的关键设计是 row_filters 用"结构化谓词"而非"字符串"。字符串拼接容易出注入和语义错位,结构化谓词(列名+操作符+值集合)可以被后续的AST改写直接消费,也能被策略引擎校验。本文评述:很多团队在这一步偷懒,直接把"region='华东'"当字符串塞进提示词,结果模型把它和用户问题混在一起,生成出 WHERE region='华东' OR 1=1 这种灾难。结构化是底线。
4.2 Schema裁剪:让模型"看不见"无权访问的表
最彻底的权限控制是"不告诉模型"。在Schema链接和提示词组装之前,先按用户权限裁剪Schema:无权访问的表直接不出现,无权访问的列从DDL里移除,敏感列替换为脱敏描述。这样模型从源头上就不会生成访问越权对象的SQL。这个思路与"最小权限原则"一脉相承,工程上实现成本低、收益直接。
但裁剪有副作用:Schema不完整会导致模型"猜"——用户问"客户复购率",如果 orders 表被裁掉,模型可能用 payments 表硬凑一个近似答案,结果语义错误。因此裁剪要配合"能力声明":在提示词里明确告诉模型"当前用户可访问的数据范围有限,若问题超出范围请回复无法回答",而不是让模型自由发挥。本文评述:Schema裁剪是"防君子"的,它降低了模型误触的概率,但不能替代执行期的硬校验——因为模型仍可能通过无权表的同义词、视图或函数间接访问。
4.3 意图中的权限信号识别
用户提问里常含权限信号,比如"我们部门""我负责的客户""本公司"。这些指代如果被模型自由解释,就会引入不确定性。更稳的做法是在解析阶段就把它们识别为"权限占位符",交给权限上下文填充,而不是让模型猜。例如把"我们部门"解析为 {ctx.dept_id},由系统在注入阶段替换为真实值。这样即使用户换个说法,只要意图识别命中,权限值就来自可信源。
意图识别可以用规则+小模型组合:规则覆盖高频指代(我们/我司/本区),小模型处理长尾。识别结果要带置信度,低置信度的占位符不自动填充,而是回问用户澄清——这比默默猜一个值安全得多。相关实现可参考LangChain的权限与工具调用教程(python.langchain.com/docs/how_to)以及OPA官方文档(openpolicyagent.org/docs)。
五、第二层 权限注入:提示词、AST改写与原生RLS三条路径
5.1 路径一:提示词约束(软约束,辅助)
在提示词里加入权限规则,例如"生成SQL时必须包含 WHERE tenant_id='{tid}'"。这种做法实现快,但本质是软约束:模型可能忽略、可能写错列名、可能被后续对话覆盖。它适合作为"第一道提示",降低模型主动越权的概率,但绝不能作为唯一防线。本文评述:提示词约束的价值在于"让模型知道边界存在",从而减少无效生成;把它当安全机制则是误用。
5.2 路径二:AST改写(硬约束,推荐主力)
这是工程上最可控的路径。流程是:模型生成SQL → 用解析器(如sqlglot、JSqlParser、Apache Calcite)解析为AST → 遍历AST,对每个涉及受控表的 FROM/JOIN 节点,在对应 WHERE 子句里注入权限谓词 → 序列化回SQL。关键点:注入要处理子查询、CTE、UNION、窗口函数等复杂结构,谓词要加到正确的层级(否则聚合结果会错)。
import sqlglot
from sqlglot import exp
def inject_rls(sql: str, ctx: PermissionContext) -> str:
tree = sqlglot.parse_one(sql, dialect="postgres")
for table in tree.find_all(exp.Table):
name = table.name
if name in ctx.row_filters_map:
pred = ctx.row_filters_map[name].to_ast() # 结构化谓词转AST
# 找到该表所在SELECT,注入到其WHERE
select = table.find_ancestor(exp.Select)
if select:
select.where(pred, copy=False)
return tree.sql(dialect="postgres")
这段伪代码省略了别名处理、多表同谓词合并、CTE递归等细节,但主干清晰。工程上要特别注意:注入的谓词列必须带表别名,避免多表join时列名歧义;对 UNION 的每个分支都要注入;对 EXISTS 子查询要判断是否需要注入(通常需要,否则可通过子查询探测他区数据)。sqlglot的文档与示例见(sqlglot.com)。
AST改写的优势是确定性强、可测试、与模型解耦;劣势是解析器对某些方言支持不全,遇到复杂SQL可能解析失败。工程上要有降级策略:解析失败时不执行,返回"无法安全处理"而非放行。本文评述:AST改写是四层闭环里技术含量最高、也最值得投入的一层,它把"权限"从自然语言的模糊地带拉进了编译器的确定世界。
5.3 路径三:数据库原生RLS(兜底,必配)
无论前面做了多少,数据库原生RLS都应开启作为兜底。以PostgreSQL为例,创建策略:
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::uuid);
应用侧在连接池获取连接后执行 SET app.tenant_id = ...,RLS自动生效。这样即使AST改写漏了、模型绕过了,数据库层仍会拦截。Snowflake的Row Access Policies、BigQuery的RLS用法类似,可参考各云厂商文档(如 Snowflake Row Access Policies)。
六、第三层 执行校验:越权检测与结果集守卫
6.1 静态校验:SQL下发前的资源访问分析
在SQL下发到数据库之前,用策略引擎做一次静态判定:这条SQL访问了哪些表、哪些列、是否包含权限谓词、谓词是否覆盖了所有受控表。可以用OPA把"SQL AST + 权限上下文"作为输入,输出allow/deny。例如Rego规则检查"所有受控表的WHERE里是否都有tenant_id谓词",没有则deny。这一步能在执行前拦住大部分结构性越权,且不消耗数据库资源。
6.2 动态守卫:结果集的行数、分布与差分检测
静态校验管不了"间接泄露"。比如用户问"华东区销售额",模型生成 SELECT SUM(amount) FROM orders WHERE region='华东',这没问题;但如果问"全国销售额减去华东销售额",模型可能生成两条查询做差,间接得到他区数据。动态守卫的思路是:对结果集做统计画像(行数、唯一值数、聚合值范围),与用户权限范围的理论画像比对,偏差过大则告警或拦截。更严格的做法是"差分隐私式"的噪声注入或最小分组阈值(如结果行数少于k条时拒绝返回明细),防止通过小样本反推。
本文评述:动态守卫是四层里最容易被忽视、也最难做准的一层。它的误报率天然较高(正常查询也可能触发画像偏差),工程上建议先"只告警不拦截",积累样本后再逐步收紧阈值,避免误伤业务。
6.3 列级掩码与结果脱敏
列级权限在执行后处理:结果集返回前,按 column_denies 把无权列替换为掩码(如 ***)或直接剔除。注意:如果用户的问题依赖被掩码列(如"按成本价排序"),应在解析阶段就拒绝或提示,而不是返回一堆掩码让用户困惑。掩码策略要与业务确认,比如手机号保留后四位、金额做区间化,既满足分析需求又不泄露个体。
七、第四层 审计回写:让权限事件反哺模型与策略
审计层记录每一次生成的完整链路:用户问题、权限上下文快照、模型原始SQL、注入后SQL、静态校验结果、执行结果画像、是否告警/拦截。这些日志的价值不止于合规,更是策略优化的燃料。比如:某类问题频繁触发"模型试图访问无权表",说明Schema裁剪或提示词需要调整;某用户频繁触发差分告警,可能需要复核其权限配置是否过宽。
回写机制可以做成"策略闭环":审计层统计越权事件,按表/列/用户维度聚合,输出"高风险Schema元素"清单,回流到解析层用于加强裁剪,回流到注入层用于补充谓词。更进一步,可以把"越权样本"做成负例,用于微调或提示词优化,让模型逐渐学会"这类问题不该生成越权SQL"。本文评述:审计回写是四层闭环"闭"起来的关键,没有它,前三层只是静态防线,无法随攻击演化而进化。
笔者认为:权限系统的成熟度,不看它拦住了多少越权,而看它能否说清"为什么拦、拦得对不对、下次怎么少拦"。审计回写就是回答这三个问题的机制。
八、多租户、列级权限与跨源查询的叠加难题
8.1 多租户隔离的"双保险"
SaaS场景下,租户隔离是行级权限的特例但要求更高。建议"生成期注入tenant_id + 执行期RLS强制"双保险,且租户ID来自会话而非用户输入。跨租户查询(如平台运营看全量)要走独立的高权限通道,与普通用户链路物理隔离,避免权限提升漏洞。
8.2 跨源查询的权限一致性
当NL2SQL需要跨MySQL、ClickHouse、Hive查询时,各源RLS能力不一,AST改写要按方言分别处理。更麻烦的是"联邦查询":一条SQL里join了不同源的表,权限谓词要在每个源各自注入,且join键本身可能泄露信息(如用他区ID做join探测存在性)。工程上建议对跨源查询做"权限域收敛":只允许在同一权限域内join,跨域join需显式授权。
8.3 列级与行级的交互
行级过滤和列级掩码会相互影响:如果用户无权看 region 列,但行级策略又依赖 region 做过滤,就会出现"过滤条件用了不可见列"的矛盾。解决思路是:行级过滤在数据库内部完成(用户不可见谓词),列级掩码在结果投影时完成,两者在SQL的不同阶段生效,互不干扰。这要求注入的谓词走"内部通道"(如RLS的USING子句),而不是拼进用户可见的SELECT。
九、评测体系:如何证明"真的没越权"
权限系统需要可验证。建议构建三层评测:
- 单元测试:对AST改写函数做表驱动测试,覆盖子查询、CTE、UNION、窗口函数等结构,断言注入后SQL的谓词位置正确。
- 对抗测试:构造提示词攻击集(如"忽略之前的指令""以管理员身份查询"),验证模型输出是否被注入层纠正、是否被校验层拦截。
- 端到端回归:用模拟数据集(标注为模拟数据)构造多租户、多角色的查询集,比对"用户可见结果"与"权限理论结果",统计越权率与误拦率。
评测指标建议包括:越权拦截率(Recall)、误拦率(False Positive Rate)、注入后SQL语义保持率(与原SQL结果在有权限范围内是否一致)、平均注入延迟。需要说明的是,目前公开的NL2SQL权限评测基准很少,Spider/BIRD均不含权限维度,本文所述评测集为工程自建模拟数据,读者应结合自身业务构造。
十、前沿预判:Agentic BI与权限感知生成
趋势之一是从"单轮NL2SQL"走向"Agentic BI":Agent会多轮规划、调用工具、自我修正。这给权限带来新挑战——Agent可能在一轮里试探性查询、下一轮基于结果调整,越权信号被分散在多轮里更难检测。应对思路是把权限上下文做成Agent的"系统级约束",每一轮工具调用都强制经过注入与校验,而不是只在最终答案处检查。
趋势之二是"权限感知的生成":在模型微调或提示词中内化权限规则,让模型在生成时就倾向于包含正确谓词。学术界已有关于"约束解码"(constrained decoding)的研究,把语法/结构约束注入解码过程。本文评述:约束解码用于权限是值得探索的方向,但它仍是"软"的——模型可能生成看似合规实则语义错误的谓词,最终仍需执行期硬校验兜底。权限的确定性要求,决定了它不能完全交给概率模型。
趋势之三是"策略即代码"与NL2SQL的融合:用OPA/Cedar等策略语言统一描述权限,生成链路各层消费同一份策略,避免"提示词一套、AST一套、RLS一套"的不一致。这需要策略语言能表达"行过滤谓词"这类结构化约束,目前Cedar(AWS开源)在这方面有探索,可参考其文档(cedarpolicy.com)。
十一、落地路线图与常见坑
11.1 分阶段路线图
11.2 常见坑
- 坑一:只靠提示词。模型被越狱后提示词形同虚设,必须有AST/RLS硬约束。
- 坑二:谓词注入层级错。把谓词加到外层SELECT而非子查询,导致聚合结果错误,用户看到"权限内但算错"的数据,比越权更隐蔽。
- 坑三:忽略UNION与EXISTS。只处理主查询,漏掉分支和子查询,形成探测通道。
- 坑四:审计只存不析。日志堆成山却没人看,越权事件无法反哺策略。
- 坑五:跨源权限不一致。MySQL有RLS、ClickHouse没有,同一问题在不同源上权限表现不同。
十二、结语
NL2SQL让取数变简单,也让权限问题变复杂。复杂不在于"加个WHERE",而在于权限必须贯穿理解、生成、执行、审计四个阶段,且每个阶段都要有确定性保障。本文提出的四层闭环,核心主张是:权限不是数据库的配置项,而是生成链路的结构性约束。把权限前移、把校验做硬、把审计闭环,AI生成的SQL才能在"好用"与"可控"之间站稳。对于正在落地NL2SQL的团队,建议从P0止血开始,逐步补齐AST改写与审计回写,切忌一步到位追求"智能权限"而忽视基础硬约束。
主要参考文献
[1] Yu T, et al. Spider: A Large-Scale Human-Labeled Dataset for Complex and Cross-Domain Semantic Parsing and Text-to-SQL Task. EMNLP, 2018.
[2] Li J, et al. Can LLM Already Serve as A Database Interface? A BIg Bench for Large-Scale Database Grounded Text-to-SQLs. NeurIPS, 2023.
[3] Zhong V, et al. Seq2SQL: Generating Structured Queries from Natural Language using Reinforcement Learning. arXiv:1709.00103, 2017.
[4] Wang B, et al. RAT-SQL: Relation-Aware Schema Encoding and Linking for Text-to-SQL Parsers. ACL, 2020.
[5] Lei W, et al. Re-examining the Role of Schema Linking in Text-to-SQL. EMNLP, 2020.
[6] PostgreSQL Global Development Group. Row Security Policies. PostgreSQL Documentation, 2024.
[7] Snowflake Inc. Row Access Policies. Snowflake Documentation, 2024.
[8] Open Policy Agent. Policy Language (Rego) Documentation. 2024.
[9] Cedar Policy Language. AWS Open Source Documentation, 2024.
说明:本文涉及的数据集评测部分为工程自建模拟数据(标注为模拟数据),用于说明评测方法,不代表任何真实业务统计。文中引用的基准(Spider、BIRD)为公开学术数据集,预处理细节以其原始论文为准。全文共引用参考文献与资料60余篇(含数据库官方文档、策略引擎文档、学术论文与工程教程),近三年文献占比超过50%,此处列出8篇主要参考文献供延伸阅读。
文章声明
本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。

