本文是「企业问数系统落地」系列第十二篇。前面十一篇讲的是答得对不对、快不快、贵不贵,这篇讲一个在选型阶段最容易被一句话带过、上线后最容易被审计问住的问题:这套系统到底能碰到多少数据、以什么身份碰、碰完留下什么痕迹。
一、先纠正一个认知:安全不等于"内网部署"
在项目里我听过最多的一句安全结论是:
"我们内网部署,数据不出网,安全没问题。"
这句话对了一半。内网部署解决的是传输安全与网络边界,它让数据不离开你的机房。但它完全解决不了另一类风险——边界内的越权访问。
举个具体的例子。系统在内网、账号只读、网络隔离做得很好,然后一位业务人员在对话框里输入:
把公司所有人的工资明细列出来请求完全合法:内网地址、只读账号、没有攻击特征。但如果系统真的答了,这就是一次严重的数据泄露——而且是从一个"设计上安全"的系统里流出来的。
所以结论很明确:安全必须分层做。内网部署只是其中一层,任何单点措施都会漏。下面给出一个可落地的五层模型,再逐层拆。
二、五层安全模型总览
| 层次 | 解决什么问题 | 主要手段 |
|---|---|---|
| 接入层 | 用什么身份连数据库、能连到什么 | 专用只读账号、最小权限授权、凭证管理与轮换 |
| 语义层 | 能问什么对象、问到什么颗粒度 | 对象可见性、行级过滤注入、指标白名单 |
| 执行层 | 生成的查询会不会越界或跑飞 | SQL 白名单校验、只允许 SELECT、强制行数上限、超时熔断 |
| 输出层 | 结果给谁看、能带出什么 | 字段脱敏、导出管控、缓存按身份隔离 |
| 审计层 | 出了事能不能追溯、排障有没有抓手 | 查询记录、提问人、执行过程、返回规模 |
这张表有一个容易被忽略的设计取向:权限收口应该尽量往"语义层"放,而不是只放在数据库层。
原因在于两者面对的抽象层级不同:
- 数据库层能表达的是"能不能读
t_contract这张表"; - 语义层能表达的是"这个角色能问'合同'这个业务对象,且只能问到本园区的合同"。
前者是技术权限,后者才是业务权限。只有技术权限的系统,最终往往被迫给一个大账号——因为业务角色和表权限根本不是一一对应的。
下面逐层展开。
三、接入层:最小权限不是"只读"两个字
很多项目对"最小权限"的理解止步于"给一个只有 SELECT 权限的账号"。这远远不够。一个可交付的最小权限设计至少要覆盖六个维度:
① 账号隔离。一个项目一个账号,不复用、不共享。共享账号的直接后果是:审计日志里只有"某账号查了敏感数据",追不到具体的人。
② 授权粒度:优先视图,而非基表。把需要查询的数据封装成视图再授权,好处是:
- 视图里可以预先剔除敏感列、预先做行过滤;
- 基表结构变化时,改视图不影响上层;
- 授权清单一眼看得懂(业务视图名,而不是几十张表名)。
③ 列级控制。敏感列(身份证、手机号、薪资类字段)默认不授权。需要时也优先授掩码后的视图列,而不是原始列。
④ 严格只读。不仅要禁 INSERT / UPDATE / DELETE,还要考虑:
- 禁用 DDL;
- 禁用可写型临时表操作(如某些引擎的
CREATE TABLE AS); - 关闭可能产生副作用的内置函数。
⑤ 网络与来源限制。账号只允许从平台服务所在网段连接,而不是"任意内网地址都可连"。
⑥ 凭证管理。凭证不进代码仓库、不进配置文件明文,走配置中心或密钥管理,并支持轮换。这一点在私有化项目里经常被忽视——最后变成某份 Excel 里存着一串密码。
示意性的授权语句(仅示意,各数据库语法差异较大,请以实际环境为准):
-- 示意:创建只读账号(示意语法) CREATE USER qa_reader WITH PASSWORD '******'; -- 示意:只授予业务视图的查询权限,而非基表 GRANT USAGE ON SCHEMA biz_view TO qa_reader; GRANT SELECT ON biz_view.v_contract TO qa_reader; GRANT SELECT ON biz_view.v_room_status TO qa_reader; GRANT SELECT ON biz_view.v_bill_receivable TO qa_reader; -- 示意:显式回收默认权限(不同引擎策略不同) REVOKE CREATE ON SCHEMA public FROM PUBLIC;连接管理的正确姿势:先把"能连、能查到什么"验证清楚,再谈使用。连接配置集中维护,新系统接入时先做试查,确认权限边界与预期一致。
四、语义层:把权限收口到"业务对象"
这是我个人认为最值得投入的一层,因为它是技术权限和业务权限之间的翻译器。
语义层要管三件事:
4.1 对象可见性
角色能看见哪些业务对象。关键机制是"不暴露就不存在":
- 薪资对象没有登记到语义层,那么"所有人的工资"这个问题在解析阶段就找不到对应对象,系统直接回"没有相关数据",而不是试图去猜某个表;
- 这比在数据库层拦截更早、更干净,也更可解释。
4.2 行级过滤注入
数据权限("只能看本部门的""只能看本园区的")在查询生成阶段自动注入过滤条件,而不是依赖用户自觉加条件,也不依赖数据库的行级安全策略(很多客户环境并不支持或不愿启用)。
一个示意性的配置(示意,非真实格式):
# 示意:业务对象与行级权限 object: contract columns: - room_id - tenant_name - amount row_filter: by_role: park_manager: "park_code IN (:user_park_scope)" # 只看本园区 dept_viewer: "dept_code IN (:user_dept_scope)" # 只看本部门 group_admin: "1=1" # 集团口径,不过滤 sensitivity: normal这样做有三个好处:
- 权限随对象走,新加一个助手时不用重新设计权限;
- 过滤条件是自动附加的,不依赖提问者是否"记得加条件";
- 可审计——每次查询实际附加了什么过滤条件,日志里看得见。
4.3 指标白名单
哪些指标可以问、按什么口径算,在语义层登记。没登记的指标,宁可答"暂不支持",也不要让模型现场推理一个算法出来。在安全语境下,"答不出"永远优于"猜一个"。
语义层说的"权限"和数据库说的"权限"不是一回事:前者决定业务上能问什么、问到多细,后者只决定技术上能读哪张表。
五、执行层:拦住"越权提问"和"查询跑飞"
先建立一个认知:在问数场景里,安全威胁的主要形态不是传统的 SQL 注入,而是"越权提问"。
原因是链路变了——SQL 不是用户写的,是模型生成的。用户手里没有输入框去注入,但他可以做两件事:
- 用自然语言诱导模型生成一个范围过大的查询("把所有历史数据都列出来");
- 通过多轮对话逐步试探(先问 A 表有哪些列,再问某列的全部取值)。
所以执行层的防护重点在生成的查询下发之前。建议设五道闸:
| 闸 | 做什么 | 拦什么 |
|---|---|---|
| ① 语法白名单 | 只允许单条 SELECT,禁多语句、禁注释绕过、禁 DDL/DML 关键词 | 拼接攻击、语句堆叠 |
| ② 对象校验 | 查询中出现的表和列必须全部在授权清单内,逐个比对 | 探测未授权对象 |
| ③ 强制行数上限 | 自动追加 LIMIT,超限直接截断并提示 | 全表拉取、批量外带 |
| ④ 超时与资源熔断 | 执行超时、扫描行数/数据量超阈值即中断 | 慢查询拖垮生产库 |
| ⑤ 并发配额与队列 | 按角色限并发,超出排队(与性能篇同源) | 突发流量、个别用户刷查询 |
第 ② 道闸是最关键的一道,也是最容易被省掉的一道。它的实现不复杂:拿到查询语句后做一次解析,提取全部表名与列名,与"本次身份 + 本次意图对应的授权对象集合"做差集,非空即拒绝。
这里有一个工程细节值得强调:授权集合应该由"语义层对象"推导出来,而不是维护一份独立的表清单。否则每加一个业务对象就要同时改两处,迟早不同步。
回答规则(Prompt 模板)是"约束生成"的地方:约定选数据、写查询、解释结果的要求。它是执行层校验的前置防线——先约束生成得规矩,再做下发前校验。
六、输出层:结果本身也是数据
前面四层都在管"怎么查",这一层管"查出来的东西怎么出去"。三个容易被忽略的点:
6.1 缓存必须按身份隔离
这是实际项目里踩过坑的地方。问数系统通常会有结果缓存或语义缓存(相同/相似问题复用上一次的查询与结果),用来省延迟和成本。但如果缓存键只包含"问题文本 + 数据版本",不考虑身份与权限上下文,就会发生:
A 用户(集团管理员,可见全部园区)提问"各园区空置情况"→ 结果入缓存 B 用户(某园区管理员,只能看本园区)问同一句话 → 命中缓存 → 看到全部园区数据这是一次不经过任何数据库层权限的越权泄露。修法很简单:缓存键必须包含权限上下文标识(角色 + 数据范围 + 敏感级别),或者更保守地——只对"授权集合完全相同"的身份共享缓存。
6.2 敏感字段掩码与实时判断
手机号、证件号一类字段,最好在输出层统一掩码,而不是在每一处查询里各写一遍。输出层掩码的好处是策略集中、便于调整,且能覆盖"通过明细导出的方式绕过去"的路径。
6.3 明细导出要管控
明细导出是最容易被忽视的外带通道。建议:行数上限 + 超限审批 + 导出行为留痕。特别注意区分"页面上看"和"导出成文件"两种权限——很多系统对前者做了控制,对后者没有。
三层答案(结论 → 图表 → 明细与来源)在安全语境下还有一个额外价值:每一次回答都自带出处。当有人质疑某个数字时,核对路径是现成的,不必再向数据团队申请一次取数——这本身也减少了"临时申请查看敏感数据"的场景。
七、审计层:别把它当合规摆设
很多项目把查询日志理解为"等保要求的合规动作"。实际上它是三类工作的共同基础设施:
用途一:安全审计。谁在什么时候问了什么、系统实际执行了什么查询、返回了多少行。当发生"某个敏感数被谁看过"的追问时,只有这个能回答。
用途二:故障排查。这是日常最高频的用途。用户反馈"这个数不对",需要能顺着记录看到:解析成了什么对象、召回了哪些样例、生成了什么查询、数据来源是哪几张表、执行耗时多少。没有过程记录的排查,只能靠猜。
用途三:运营优化。哪些问题被问得最多、哪些问题反复答不上来、哪些对象长期没人问——这些决定了下一步该补样例、补口径还是补培训。
一个可参考的记录字段清单:
时间戳 / 提问人 & 角色 / 原始问题 / 解析出的业务对象与指标 命中的样例(若有)/ 生成的查询(含注入的行级过滤条件) 数据来源对象 / 执行耗时 / 返回行数 / 是否截断 / 最终状态注意最后几项:"是否截断"和"最终状态"必须记。一个被 LIMIT 截断却回给用户"完整结果"的系统,比答错更危险。
八、部署边界:私有化解决了一半问题
私有化/内网部署解决的是数据不出网。但如果方案里包含外部模型 API 调用,就要多问几句:
- 送出去的是什么?最理想是只送"结构性信息"(字段名、类型、脱敏样例),不送真实业务数据值;
- 送出去的结构信息本身敏感吗?表名和字段名经常携带业务信息(比如
salary_detail、layoff_list),这本身就是泄露; - 能不能换成项目内模型?这是"模型可选择"这个设计取向的实际价值——模型是可替换部件,不是被绑定的黑盒,这决定了长期的合规弹性。
最后一句提醒:安全设计要写进项目配置与运行管理,而不是只写在方案文档里。方案里写"采用只读账号",和项目里真的有一个权限收紧过的账号、有凭证轮换机制、有可控的授权清单——是两回事。
九、五个常见反模式
| 反模式 | 为什么危险 | 换一种做法 |
|---|---|---|
| 全项目共用一个"超级只读"账号 | 审计日志无法定位到人,一次泄露影响全部数据域 | 一项目一账号;必要时按角色拆分账号 |
| 直接把业务基表暴露给查询 | 敏感列裸露;表结构变更影响上层;授权清单无法阅读 | 用视图封装 + 列级控制 |
| 只做网络隔离,不做语义层权限 | 内网内的越权提问无任何拦截手段 | 语义层做对象可见性与行级过滤注入 |
| 缓存键不含权限上下文 | 一次缓存命中就是一次越权泄露 | 缓存键纳入角色与数据范围 |
| 查询日志只记成功/失败 | 出问题无法定位;审计问不住 | 记录解析结果、查询语句、来源对象、返回规模 |
十、落地检查清单
按上线顺序排列,每一条都应该是"验证过的",而不是"设计过的":
- 只读账号已创建,且实测确认无法执行任何写操作;
- 授权对象为视图(或同等封装),授权清单可被人读懂;
- 敏感列已确认未授权,或已通过掩码视图暴露;
- 账号来源限制在平台服务网段内,实测其他网段连不上;
- 凭证不在代码/配置文件明文,具备轮换路径;
- 语义层已完成对象可见性登记——未登记对象提问时明确返回不支持;
- 行级过滤已在语义层配置,且验证过不同角色看到不同范围;
- 执行层五道闸全部开启,并实测"越权提问被拒"的用例;
- 缓存键已包含权限上下文,且用双角色用例验证过不串数据;
- 输出层掩码与导出管控生效,页面可看 ≠ 可导出;
- 查询记录字段完整(含"是否截断"与最终状态);
- 至少准备 5 个"越权提问"测试用例,纳入每次配置更新后的回归验证。
第 12 条值得单独说:安全用例和业务用例一样,必须进回归集。否则一次"顺手放宽权限",可能悄无声息地推翻前面十一条。
小结
回到开头那个问题:"内网部署就安全了吗?"
答案是:内网部署解决边界,只读账号解决身份,语义层解决业务权限,执行层解决查询范围,输出层解决数据外带,审计层解决可追溯。六件事分别对应六类风险,缺哪一块,风险就从哪一块进来。
如果只能记住一句话,我希望是这句:
安全不是给系统加一个开关,而是让每一个问题在被回答之前,先经过一次"这个身份该不该看到这个数据"的判断。
而这个判断,最好发生在业务对象这一层——因为它既是业务语言,又能被机器执行。
系列文章
| 篇 | 主题 |
|---|---|
| 01 | 技术架构解析 |
| 02 | 智慧园区落地复盘 |
| 03 | 口径治理实践 |
| 04 | 选型评估指南 |
| 05 | 问答样例库设计 |
| 06 | 语义层建模 |
| 07 | 接入层实践 |
| 08 | 提示词工程化 |
| 09 | 多轮追问与上下文 |
| 10 | 评测与度量 |
| 11 | 性能与成本 |
| 12 | 数据权限与安全工程(本文) |
说明:文中架构、配置与代码均为示意,用于表达设计思路,不代表产品实际实现细节;截图中的业务数据均为演示环境数据;安全策略需结合客户实际环境与合规要求确认后落地。