第 4 章 密钥与信任边界:谁能碰模型
难度 ★3 | 约 3.5 学时 | 6 节
这一章只回答一个问题:在你和模型之间,哪一段必须由你的服务端来走。 答案不是「为了安全」这种口号,而是一张能标出密钥、数据与权限三样东西落点的图。
学习目标:画出「谁能碰密钥、谁能碰提示词、数据从哪出境」的信任边界,并给出可执行的落地方式。
本章交付物:一张信任边界与数据流向图(密钥位置 / 数据出境点 / 权限来源)
4.1 API Key 绝不能进浏览器:打包进前端等于公开发布
这一节不给禁令,给机制。因为禁令只能让人照做,机制才能让人自己判断任何新方案——包括下周同事提出的那个「看起来很聪明的」方案。
机制只有一句:前端构建产物是公开可下载的。
这句话的意思是:打进前端的每一个字符串,最终都会出现在浏览器能读到的文件里。压缩、混淆、拆包、改个变量名——这些动作只增加阅读成本,不构成保护。一个人打开开发者工具,找到那个文件,搜索几十个字符,就能把密钥拿走。成本是几分钟,不是「很难」。
由此可以直接推出一个等式:把密钥打包进前端 = 公开发布这个密钥。 不是「有泄露风险」,是已经公开了,只是还没人来看。
三个最常被当成例外的方案,用同一条机制都能判掉:
① 「拆成几段拼起来」。 拆几段都是字符串,都在同一个文件里。拼接发生在运行时,读取发生在读取时——读取比拼接更早。
② 「混淆之后再放进去」。 混淆改变的是可读性,不是可达性。而且前端混淆的产物要能被浏览器执行,就必须能被浏览器还原成可执行的代码。
③ 「构建期注入的环境变量」。 这个最危险,因为它看起来像「服务端的事」。但注入的落点仍然是浏览器能读到的那个文件。判断标准只有一条:这个值最终写进了哪个文件,那个文件谁能下载。
那正确的形状是什么?框架的约定已经替你想好了:只有服务端代码能读到环境变量,而带公开前缀的环境变量会被注入到浏览器端。这不是命名习惯,是加载机制——前缀就是那条分界线。实践侧把它写成一句铁律:「永远不要在客户端代码里暴露密钥或提供方机密——要用只在服务端运行的路由处理器」,并补了同源的半句:「永远不要在客户端组件里直接调用模型 SDK 函数」。
所以本节落成三个自检动作,每一个都能在一分钟内做完:
① 找位置。 密钥只允许出现在服务端环境变量里;代码里搜一遍它出现的所有位置,凡是落在客户端目录或构建入口可达的地方,一律挪走。
② 找调用。 客户端只调自家接口,不直接调提供方。判据很硬:在浏览器网络面板里,你不应该看到提供方的域名。
③ 找残留。 提交前与发版前各扫一遍:带公开前缀的变量名、构建产物里像密钥的字符串(长串高熵、常见前缀)。这一条要进 CI,因为人是会忘的。
最后一句:「密钥放哪」不是安全问题,是架构问题。 它决定你有没有一个服务端。而这恰好是下一节要讲的那一层为什么必须存在。
4.2 提示词是资产,也是攻击面——但攻击面不止提示词
先立住「资产」这一半,因为它很好办:提示词要进版本库、要能 diff、要能回滚。
理由和代码一样——它决定行为,所以它必须可追溯。 一个不能 diff 的提示词库,等于一个不能 code review 的代码库:你知道它现在长什么样,但你说不清它上周为什么变好、这周为什么变差。
「也是攻击面」这一半,大纲写的是「它会泄露、会被注入」。方向对,但范围窄了一圈。
这条不是我挑的毛病,是官方自己动手改的:风险清单在 2026 版里做了一件很能说明问题的事——把上一版的条目「系统提示词泄露」改名并扩大成「隐藏上下文暴露」。新名字覆盖的不只是那段系统提示词,还包括检索回来的文档、记忆、用户信息、应用状态与工具响应。清单给的理由是,这个名字「更能反映当今一个 AI 应用在上下文里能装什么」。
请把这句话读第二遍:会泄露的不是「一段文字」,而是「装进上下文的所有东西」。
这不是名词之争,术语的边界就是读者动作的边界。 按旧范围写,读者会只去保护那段系统提示词,然后在别处漏个干净:
① 检索回来的文档。 它可能来自一个用户可编辑的 wiki。你把它拼进上下文,它就成了指令的一部分(第 3.5 节的另一头)。
② 记忆。 上一轮存下来的偏好、结论、片段。它跨会话存活,所以它的泄露面也跨会话。
③ 用户信息与应用状态。 当前订单号、账户状态、权限标签。它们不是「内容」,但它们在上下文里。
④ 工具响应。 一个内部接口的返回体。它可能是最敏感的那一项,因为它通常没人审。
于是保护清单也要跟着改:从「别把系统提示词抄出去」变成「逐项列出哪些东西会进上下文,并给每一项定一个可见范围」。这一步和第 4.6 节那张流向图是同一件事的两个视角。
最后补一句时效:这条改名发生在 2026 版。引旧名(「系统提示词泄露」)会让你的引用对不上——而按旧名做的保护,恰好会漏掉新增的那四项。
4.3 服务端的三个职责:持有密钥、拼装提示词、做出口校验
这一节要先把一句话的身份说清楚,否则读者会以为它是官方口径。
官方的架构文档里,服务端确实承担模型侧职责,它列的清单是:认证检查、提示词构造、提供方调用、日志与策略执行。请注意这份清单的三个特征:它比三条长;它没有把「出口校验」单独拎出来(校验在官方口径里属于应用侧的事);它也没有说「三个职责」。
所以正确的写法是:官方说服务端负责这几件事;本书把它们归成三条必守的职责。 下面这三条是本书的归纳,归并的理由也要写出来——因为每条各自对应一个「放错位置就会出事」的失败模式,而官方清单是按职能切的,不按失败模式切。
| 本书归成的三条 | 官方清单里的对应项 | 放错位置的失败模式 |
|---|---|---|
| 持有密钥 | 认证检查 + 提供方调用 | 密钥进前端 → 已公开发布(4.1) |
| 拼装提示词 | 提示词构造 | 提示词在前端 → 用户可改写,且版本不可追溯(4.2) |
| 做出口校验 | 日志与策略执行(部分) | 不做校验 → 把模型输出直接当数据用(第 3 章) |
这张表比单独一句「三个职责」信息量大,原因在于第三列:它让读者能反过来验——我这条职责现在放在哪一层?如果放在客户端,会以哪种方式坏?
还有两件常被漏掉的事,都属于「既然服务端要扛,就得扛全」:
① 服务端要能回答「这次调用是谁发起的」。 没有这个身份,4.5 节的权限就无从谈起——你只能拿服务账号全权去查。
② 服务端要留下可追溯的记录。 记录不是日志越多越好,而是每次调用能对上:谁、什么时候、用了哪个版本的提示词、花了多少 token、结果被谁用了。第 14 章会把这条做成一条调用链。
最后一条边界:前端在这套分工里的位置是清楚的——它只负责展示与交互,通过自家端点消费流。 它不持有密钥、不拼提示词、不判权限。这三件事一旦有一件滑到前端,这一节的表就会告诉你它会怎么坏。
4.4 数据最小必要:脱敏要做在发送之前,而出境要能判定
这一节的第一句最不可让步:脱敏要做在发送之前。
理由不是「合规要求」,是物理事实:数据一旦离开你的边界就不可撤回。事后删除的承诺,不等于当时没发。所以「先发出去、发现不该发再删」这个顺序,在任何技术意义上都不成立——你能删的只是你这边的那份。
「数据最小必要」也有明文落点:合规体系把「最少够用」写成基本原则之一——数量最少、频率最低、保存时间最短,并要求出境前做个人信息保护影响评估。这三条可以直接当评审问题用:这一条字段为什么必须发?能不能少发一条?要留多久?
但大纲在这里少了一层,而这一层恰好决定读者会不会真的去做:「出境」在什么条件下成立?
很多人心里的模型是「大量传输才算出境」,于是觉得「我就发一条订单摘要,应该不算」。政策口径把这个模糊地带说得很直接:调用境外模型 API 时,输入数据经 API 传输到境外服务器处理,在大多数情况下构成数据出境。 还有一条更容易被忽略的:当境外用户在提示词里嵌入个人信息、经模型处理后回传境外,同样可能构成「向境外提供个人信息」。
请注意这两句话的共同点:判定的关键是「数据到了境外并被处理」,不是「传了多少」。 一条也是出境。
出境之后,义务是分档的。三条法定路径与它们的数量分界(这里只写形状,法律细节在第 15 章):
| 路径 | 适用分界(累计) |
|---|---|
| 安全评估 | 100 万人以上个人信息,或 1 万人以上敏感个人信息 |
| 订立标准合同并备案 / 个人信息保护认证 | 10 万人以上不满 100 万人个人信息,或不满 1 万人敏感个人信息 |
另有一条时效必须带上:《个人信息出境认证办法》自 2026-01-01 起施行,成为继「安全评估」「标准合同」之后的第三条法定路径。换句话说,「只有两条路」这个说法已经过期了。
那么工程上的正解是什么?两条,按顺序试:
① 优先境内部署。 涉及个人信息或重要数据的场景,先把「能不能不出境」这个问题问完——能不出境就不出境,这是最省事的一条,因为它把整张义务表都绕开了。
② 先脱敏再出境。 如果必须出境,就把出境这件事限制在「不含个人信息的那部分数据」上。注意这里的纪律:脱敏要在发送之前完成,且脱敏的目标不是「看起来不像」,而是不可复原——把姓名换成编号、把手机号整段去掉、把能反推到个人的组合字段拆开。
最后一句分工提示:这一节只讲「它如何约束你发什么数据出去」;完整的合规清单(标识、备案、内容安全)在第 15 章。两章别互相吞篇幅,读者也别指望在这一节拿到全部答案。
4.5 权限跟着调用者走,不跟着服务账号走
这一节是本章唯一能给出「四步可执行清单」的地方,所以写足。
先说错在哪:用服务账号的权限去替用户查数据,是最常见的越权来源。 它之所以常见,是因为它最方便——服务账号什么都能看,于是「模型能不能看到这条订单」这个问题,被悄悄地换成了「服务账号能不能看到这条订单」。而后者的答案永远是能。
现行授权规范把这件事写成了明确的角色分工,而且分工很窄:资源服务器只做三件事——验证令牌、提供元数据、返回正确的挑战。 它不签发令牌、不登录用户。「不签发」这三个字很关键:它意味着服务器没有能力「造一个身份」,只能「验一个身份」。
验证要验四条,缺一条都等于没验:
① 验签名。 对提供方的公钥集验,并且要处理密钥轮换——只缓存一份公钥的实现,会在对方轮换的那天全量失败。
② 验签发者。 这个令牌是不是我信任的那一方发的。
③ 验受众就是本服务。 规范把它叫「承重墙」,原因很实在:它区分「一个合法的令牌」与「一个给我的令牌」。 客户端必须在授权请求与令牌请求里都带上资源参数;服务器必须验证令牌的受众就是自己,否则一律拒绝。
④ 验作用域覆盖当前这次操作。 不是「这个令牌有没有权限」,而是「这个令牌有没有权限做这一件事」——落到工具上,就是每个工具一个最小权限。
与这四条配套的,是两种返回码的分工,别混用:
| 情形 | 返回 | 客户端该做什么 |
|---|---|---|
| 缺令牌 / 令牌无效 | 401(带挑战响应头指向元数据) | 去取元数据、走授权流程 |
| 令牌有效但权限不足 | 403 | 发起升级授权(这是信号,不是故障) |
现在说那个最常见的反面模式:令牌透传。
它的形状是「我收到了一个令牌,正好下游也要令牌,那我原样转过去」。看起来很像是正常复用,规范却明令禁止。理由是一个很具体的名词:混淆代理——一个为服务 A 签发的令牌被用来访问服务 B,下游审计里记着的是错误身份。也就是说,出事之后你连「是谁干的」都查不出来。
还有两处容易漏的边界:
① 适用传输。 这套授权流程只定义给基于 HTTP 的传输。本机以标准输入输出方式启动的服务不应照此办理——它从环境里取凭据。别把远端那一套硬套到本机进程上,那会变成一套没有对手方的握手。
② 发现机制有两种入口,客户端两种都要支持。 服务器可以通过 401 的挑战响应头暴露受保护资源元数据,也可以通过一个已知路径。只实现一种的客户端,会在遇到另一种服务器时卡死。
顺带一条时效:动态客户端注册已被弃用,现行做法是「客户端把一份元数据文档发布在已知网址上、由授权服务器取回校验」。按旧教程写注册流程,会撞上一个已经不再被推荐的方向。
最后把这一节接回 3.5 节:模型能访问的数据必须按当前用户限权,而「当前用户」是从令牌里来的,不是从提示词里来的。提示词里写的「我是管理员」,在授权层没有任何意义。
4.6 一个可观测的验证:画一张数据流向图,标出每个出境点
这一节的东西是本书构造的,先说清身份:本轮检索没有找到任何机构发布过「信任边界与数据流向图」的模板、字段清单或验收标准。权限与合规类材料给的是原则与检查项,不是这种图。所以它不是行业标准做法,它是本书为了让原则能落地而设计的一个动作。
但它和官方材料是同向的——风险清单给的那套处置建议(限制工具与功能、按操作给最小权限、授权检查放在模型之外、高影响操作要人工核准),要落地的前提就是先有一张图说清「谁在什么时刻能碰到什么」。
图不用画得漂亮,它只要产出三样东西:
① 密钥位置。 在哪一层、由谁读、会不会进构建产物。这一格的验收方式是 4.1 那三个自检动作。
② 数据出境点。 每一个「把数据送出自己可控边界」的调用。注意「可控边界」是按你的组织划的,不是按机房划的——同一个云厂商的另一个区域,对你可能就是境外。
③ 权限来源。 每一次模型调用用的是谁的权限。这一格只有两种合法答案:调用者的令牌,或一个明确限权过的服务身份。写「服务账号」而说不出限了哪些权,这一格就是空的。
收口句同时也是验收方式:标不出来的出境点,就是合规风险点。
这句话的用法是反的——不是「标出来的才要管」,而是「标不出来的先不能上线」。 因为标不出来通常意味着两件事之一:要么这条链路没人知道(那它就不受任何评审约束),要么有人知道但没说(那它是刻意的盲区,比前一种更麻烦)。
最后两句话把这张图接到别处:
① 它是第 15 章合规检查项的输入。 第 15 章那些「标识、备案、内容安全」的检查,第一个问题都是「数据去了哪」——这张图就是那个答案。
② 它要跟着架构一起改。 加一个工具、换一个提供方、开一个新区域,图没改就等于没画。所以它应该和提示词、评测集放在同一个版本库里(第 12 章会把这三样归成「部署单元」)。