用户在个人中心问 Agent:“帮我查一下这个订单。”模型识别出了订单号,调用查询工具,页面显示商品和发货状态。这个流程看起来已经完成,但还缺一个判断:当前登录账号是否有权查看这笔订单?
订单号可能来自聊天、截图或复制粘贴。它只说明用户想查哪个对象,不能单独证明这个对象属于用户。即使模型没有恶意,也可能把别人提供的编号原样交给工具。
这一篇用 Vue 3、TypeScript 和本地 Node 服务做一个最小例子:工具参数只表达查什么,会话上下文决定是谁,服务端核对订单归属之后才返回结果。
全部账号、订单和商品均为公开编写的演示数据,模型固定选择只读工具,没有调用真实模型。本地 HTTP 提供可任意选择 A/B 的模拟登录,并非真实身份认证;公开演示使用浏览器内的离线规则模拟,不具备数据保密能力。

把账号从模型参数中拿出来
本例设置两个模拟账号,各有一笔订单。账号 A 查询自己的 DEMO-A-1001 可以成功,查询账号 B 的 DEMO-B-2001 则被拒绝。编号中的字母只方便读者观察,真正的权限判断来自订单记录的 owner 字段,而不是猜编号前缀。
模型生成的工具入参只有一个业务字段:
type OrderQuery = { orderId: string };
接口不接受模型指定 customerId、owner 或会话令牌。演示中的“模型伪造 customerId”按钮会额外提交账号字段,服务因参数不符合约定返回 400,而不会切到那个账号查询。
不过,去掉账号参数还不够。工具收到另一个人的有效订单号时,仍然必须做对象级权限检查。OWASP 将通过修改对象 ID 访问无权对象的问题归为 BOLA,并要求每个使用对象 ID 访问记录的接口检查相应权限。OWASP:API1 对象级授权失效
身份由服务端会话映射得到
本地 HTTP 模式中,选择模拟账号后,Node 服务生成随机令牌,在内存中记录它对应 A 还是 B,再通过 Cookie 交给浏览器。后续查询请求携带 Cookie,服务端查自己的映射,得到当前身份。
浏览器 Cookie → 服务端会话 → 当前账号
工具 orderId → 订单记录 → 归属校验
这两条输入在服务端汇合。客户端不能靠查询正文重新定义当前账号,模型也不需要知道会话令牌。
Cookie 设置 HttpOnly、SameSite=Strict、五分钟有效期。HttpOnly 限制 JavaScript 读取 Cookie,但浏览器发起请求时仍可携带它;SameSite 约束跨站发送方式。这些属性都不能代替订单归属检查。MDN:Set-Cookie 属性
示例只监听 127.0.0.1:8090,使用本地 HTTP,因此没有设置 Secure。真实部署需要 HTTPS 和相应 Cookie 配置。更关键的是,/api/demo-login 允许任意选择演示账号,不验证密码;它只用来建立不同测试身份,不能作为正式登录系统。
查询必须同时匹配对象与归属
共享服务规则在 src/service.ts。下面是实际查询实现,token 由 HTTP 适配层从 Cookie 读取,identity 只查服务端会话映射:
function query(token: string, raw: unknown): Order {
const current = identity(token);
const orderId = orderInput(raw);
const found = orders.find(order =>
order.orderId === orderId && order.owner === current.account
);
if (!found) {
throw new ServiceError(404, '未找到可访问的演示订单。');
}
return { orderId: found.orderId, item: found.item, status: found.status };
}
先确认会话有效,再校验工具参数,最后查找当前身份可访问的记录。未登录、已退出或过期的会话返回 401;请求格式错误返回 400。成功时只返回页面需要的订单号、演示商品和状态,不把内部归属字段或整份会话对象交给模型。
对账号 A 来说,B 的订单和不存在的订单均返回相同的 404 与错误文案,响应里不附带另一个人的商品名称。这减少了错误内容对资源存在性的直接暴露;本例没有实现恒定耗时或完整的防枚举策略,不能由文案一致推导出不存在其他侧信道。
真实数据库查询也应受当前身份和业务权限约束。不要先把所有账号的订单发给浏览器,再靠前端过滤展示。
切换账号时清空旧结果
权限规则正确,前端仍可能展示错误的身份与结果组合。比如 A 的查询还在进行,用户已经切到 B,A 的响应才回来。如果直接更新卡片,页面就会在 B 的身份标签下面挂上旧任务结果。
本例切换账号或退出时,立即清空旧订单与待确认身份,并增加操作序号。查询的成功和失败返回,都只有在操作序号仍匹配时才能更新页面;组件卸载后也不再发布状态。
查询 HTTP 响应还携带服务端确认的展示身份,前端用它和订单一起更新,避免仅沿用本地标签。页面初始化会读取当前会话,因此刷新后可恢复仍有效的 HttpOnly Cookie 身份;收到 401 时清空本地身份并提示重新选择模拟账号。
这里没有实现跨标签页的实时身份广播。另一个标签切换 Cookie 后,本页要在刷新或下一次成功响应时校准身份;这项限制应与“服务端每次请求都会重新校验会话”分开理解。
身份显示和旧响应保护属于页面一致性,服务端授权才决定数据是否返回。即使直接绕过页面发送工具请求,服务仍然按会话核对归属。
离线演示不能证明数据保密
直接打开公开页面时,订单查询在浏览器内存中运行。两笔演示订单都被打包在公开 JavaScript 中,读者可以检查源码看到它们。页面上的 A/B 限制只是演示同一套业务判断,没有构成真正的数据隔离。
源码包还提供真实本地 HTTP 路径。浏览器通过同源接口请求 Node,会话映射与订单查询在服务进程内执行,权限失败时响应不会包含他人订单。这个路径可以用来检查 Cookie 和工具接口的传输边界,仍然不能证明真实认证、真实数据库或生产权限体系已经完备。
HTTP 服务只开放固定页面与接口,检查 Host、Origin、JSON 类型和请求体大小,不提供 CORS。订单工具只读,没有修改订单、退款或发送消息。进程重启会清空会话;会话最多 64 个,五分钟后失效。
公开 GitHub Pages 不运行 Node,也不会访问读者电脑上的服务。HTTP 模式只在本地 8090 页面自动启用,其他运行地址使用离线模式,界面会标明当前路径。
用两个身份验证同一个订单号
权限测试不能只跑“账号 A 查询 A 成功”。至少要使用不同会话访问同一个对象,确认允许和拒绝来自服务端规则,而不是按钮隐藏。
| 场景 | 预期结果 |
|---|---|
| 未登录查询 | 401,不返回订单 |
| A 查询 A 的订单 | 返回允许的展示字段 |
| A 查询 B 的订单 | 404,无 B 的订单内容 |
| A 查询不存在的订单 | 与无权限请求相同的 404 正文 |
| 工具参数伪造 customerId | 400,不能切换查询身份 |
| 退出后重用旧令牌 | 401 |
| 会话过期 | 401;页面清空本地身份 |
| 查询期间切换账号 | 旧成功或错误不能覆盖新页面 |
核心测试使用可控时钟验证五分钟过期,不需要真的等待五分钟;竞争测试使用可手动完成的 Promise 指定旧响应何时到达。HTTP 测试则实际启动 Node,使用不同 Cookie 发送请求,检查状态码、响应内容与会话删除。
这些检查覆盖的是本例的固定数据与协议。对正式系统,还要列出共享订单、组织角色、代理查询等真实授权规则,逐条验证,不能把简单的 owner 相等推广为所有业务的完整权限模型。
运行两种模式并检查响应
打开订单权限演示 · 下载 Vue / TypeScript 与 Node 源码
解压后直接打开 index.html 可运行离线模拟。选择 A,依次查询当前账号、另一账号、不存在订单和伪造参数,观察结果区域是否清空、文案是否符合约定。Vue 已打包,无需模型密钥或 CDN。
要检查真实本地 HTTP,请运行:
npm run serve
打开 http://127.0.0.1:8090/,在浏览器网络面板查看工具响应。选择 A 查询 B 时,应看到 404 且没有 B 的商品内容;退出后再查询应得到 401。刷新页面也要核对恢复的身份与结果状态。
修改源码时执行:
npm ci
npm run typecheck
npm test
npm run build
npm run test:http
本次类型检查、18 组核心测试和 17 个真实本地 HTTP 检查通过。发布前还分别操作离线和 HTTP 的成功、越权、参数伪造、退出及刷新路径,检查电脑和手机页面。验证均使用模拟模型与公开演示订单,没有执行真实模型或业务调用。
把权限放在每个工具入口
真实 Agent 可能不止一个订单工具。列表、详情、物流和售后入口都可能通过对象 ID 访问记录,每一个入口都要检查当前身份的访问范围,不能因为上一轮对话查过一次,就跳过下一次检查。
会话应由正式认证系统建立,服务端校验账号、租户和业务角色;模型上下文只拿完成任务所需的信息。日志中的会话令牌与个人信息也需要控制暴露范围,不能把完整原始请求直接展示给用户。
前端负责把当前身份、等待、失败和结果解释清楚,工具服务负责决定能否访问对象。只要守住这条边界,用户给出一个订单号,Agent 也仍然需要先证明当前会话有权查询它。