应用密钥、scope 与业务用户隔离
对照真实 UI 和 Gateway 路由选择七种 scope,正确传递业务主体,并完成密钥更换、轮换和撤销。
开发者接入应用密钥验证什么#
业务后端用 App Key 调用 Gateway;桌面登录、设备连接、入站 Webhook 签名与 App Key 是不同身份通道。
每个业务请求同时包含:
Authorization: Bearer <应用密钥>
X-Onevium-Subject: <由业务后端确认的用户主体>
密钥确定 owner、应用和可用 scope;X-Onevium-Subject 指定这个应用内的业务用户。Gateway 不会替你的系统验证该用户是否登录,也不会把请求正文里的 owner_id、app_id 当成授权。
创建和取得密钥#
- 打开 渠道 → 外部接入,创建应用或选中已有应用。首次创建应用生成的密钥只显示一次,默认包含全部七种 scope。
- 为具体后端创建较小权限的凭据:在选中应用上点击创建 API 密钥。
- 逐项勾选操作,点击创建;关闭凭据弹窗前保存到后端的凭证存储。
- 在应用的凭证与历史 → API 密钥查看密钥前缀、Active/Revoked 状态,以及轮换/撤销入口。这里不会重新显示原密钥全文。
Bearer 填一次展示的完整应用密钥,不能用列表前缀或 key_id 代替;也不要填桌面登录令牌或私有控制令牌。
创建对话框的 “对话接入” 预设实际勾选全部七项;“只读” 勾选四个 :read 项。预设不是为你的业务计算的最小权限,应逐项核对。至少选择一项,未知或重复 scope 不被接受。
只读 API 密钥权限示例:可以查询状态和历史,不能发送消息或启动任务。
图中为预览界面的只读权限演示,使用隔离示例数据;不表示已创建真实密钥或通过真实业务授权验收。
七种真实 scope#
| UI 名称 | API 枚举 | 允许的操作 |
|---|---|---|
| 查看已发布服务 | endpoints:read | 列出当前应用的已发布服务 |
| 读取会话历史 | conversations:read | 列会话、读快照/历史、会话 SSE,以及查询 operation |
| 发送消息和重开会话 | conversations:write | 新建会话、发送消息、reset |
| 查看任务状态 | runs:read | 查询 run 和 run SSE |
| 提交和停止任务 | runs:write | 从服务直接创建 run、取消 run |
| 下载附件 | resources:read | 下载当前主体可访问的资源内容 |
| 上传附件 | resources:write | 预留资源并上传内容 |
没有 *、admin 或 webhooks:write 这类 App Key scope。conversations:write 包含 reset,且 reset 可以使用 cancel_and_reset 停止本代工作;它不是“只能发文字、不能停止任务”的权限。
Scope 只控制 API 操作类别,不会增加服务已经发布的工具、目录、Skills 或 MCP 能力。请求方不能通过添加 model、tools、working_directory 等未知字段来扩权。
每类公开接口需要什么权限#
以下路径均属于 Gateway 的业务 API。除 /health 和单独签名的 Webhook 外,表内接口还要求 App Key 与非空 subject。
| 方法和路径 | 必需 scope | 说明 |
|---|---|---|
GET /api/v1/agent-endpoints | endpoints:read | 当前应用的已发布服务列表 |
POST /api/v1/agent-endpoints/{id}/runs | runs:write | 任务型入口 |
POST /api/v1/conversations | conversations:write | 创建业务会话 |
GET /api/v1/conversations?endpoint_id=... | conversations:read | endpoint_id 用于定位设备 |
GET /api/v1/conversations/{id} | conversations:read | 快照 |
GET /api/v1/conversations/{id}/messages | conversations:read | 分页历史 |
POST /api/v1/conversations/{id}/messages | conversations:write | 发送消息 |
GET /api/v1/conversations/{id}/events | conversations:read | 会话 SSE |
POST /api/v1/conversations/{id}/reset | conversations:write | 重开上下文 |
GET /api/v1/runs/{id} | runs:read | 状态和结果 |
GET /api/v1/runs/{id}/events | runs:read | 单次任务 SSE |
POST /api/v1/runs/{id}/cancel | runs:write | 停止任务,请求体 {} |
GET /api/v1/operations/{id} | conversations:read | 即使 operation 来自 run cancel,也使用这个 scope |
POST /api/v1/resources | resources:write | 声明 endpoint_id、content_type、size |
PUT /api/v1/resources/{id}/content | resources:write | 上传字节;还需匹配类型、长度和 X-Content-SHA256 |
GET /api/v1/resources/{id} | resources:read | 返回文件内容,不是资源 JSON 元数据 |
GET /health | 不使用 App Key | Gateway 健康信息,不证明设备或模型执行成功 |
POST /api/v1/webhooks/{integration_id} | 使用 Webhook 签名 | 见Webhook 教程,不能只用 App Key 代替签名 |
创建会话、发送消息、直接创建 run、cancel、reset 和预留资源都需要各自操作的 Idempotency-Key。资源内容 PUT 使用内容哈希与不可变资源约束,不应把它当成创建新资源的重试。
应用和 subject 如何隔离#
同一 owner 下不同应用的服务、会话、任务和资源仍分开。读取已有资源时,会同时核对应用和 subject;换成另一个 subject 读取原用户资源,通常得到 404,不是返回别人的内容。
但 App Key 没有绑定某个 subject:持有密钥的后端有责任填入正确主体。不要直接透传浏览器提交的 X-Onevium-Subject、URL 用户 ID 或表单值。建议从已验证登录态构造稳定内部标识,并在多租户系统中包含租户维度。
下面只是业务后端伪代码,不是 Onevium SDK:
const user = requireAuthenticatedUser(request); // 你现有的登录校验
const subject = `tenant:${user.tenantId}:user:${user.id}`;
const gatewayHeaders = {
Authorization: `Bearer ${serverSecrets.oneviumAppKey}`,
"X-Onevium-Subject": subject
};
subject 必须非空,最多 256 字符且无首尾空白。显示名称和邮箱容易变化,不适合作为持久会话关联。读取、SSE、附件下载和控制操作都要沿用相同主体。
同一应用的不同 API 密钥只在 scope 上有区别,不自动隔离用户或服务。若两个业务系统连密钥持有方都不能互相访问,应使用不同应用,而不是只给同一应用多建两把密钥。
按真实功能选择最小范围#
| 后端用途 | 起始 scope 集合 |
|---|---|
| 仅查询已知 run 的结果 | runs:read |
| 直接提交独立任务并查结果 | runs:write、runs:read |
| 创建/续聊,查看历史和结果 | conversations:write、conversations:read、runs:read |
| 在上述会话流程中单独取消 run | 再增加 runs:write |
| 上传或下载附件 | 分别增加 resources:write、resources:read |
| 需要先让用户选择已发布服务 | 再增加 endpoints:read |
请求 operations/{id} 需要 conversations:read。仅配 runs:write + runs:read 可以发起 cancel 并查 run,却不能查询控制操作完成状态;需要这个完整闭环时补上 conversations:read。
用真实身份验证一次#
先从凭证存储加载 APP_KEY,把 Gateway 地址和业务主体设置为测试值。下面只读取当前应用的服务,需要 endpoints:read:
这里的 GATEWAY_URL 只填网关 origin,不包含末尾 /api/v1。 例如界面显示 http://127.0.0.1:48541/api/v1 时,变量填 http://127.0.0.1:48541;下方命令会自行追加 /api/v1,不要重复。
: "${APP_KEY:?Load APP_KEY from your credential store first}"
export GATEWAY_URL='http://127.0.0.1:REPLACE_WITH_API_PORT'
export SUBJECT='docs-test-user'
curl --fail-with-body -sS "$GATEWAY_URL/api/v1/agent-endpoints" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT"
成功应返回 200 和 endpoints 数组。空数组表示这个应用没有可见的已发布服务,不代表需要扩大到管理员权限。
建议用另建的只读测试密钥尝试一项写操作,确认收到 403;再按会话教程用两个测试 subject 检查隔离。这些是你需实际执行的验收,不是本页已完成的测试。
权限不足的示例响应(非实测):
{
"error": {
"code": "FORBIDDEN",
"message": "Application scope is not allowed."
}
}
更换、轮换和撤销密钥#
在凭证与历史 → API 密钥找到正确前缀,选择轮换密钥 → 确认轮换,保存新显示的密钥。轮换保留原 scope,并撤销旧密钥;没有给旧密钥保留宽限期。
需要平滑切换时使用两把密钥:
- 创建 API 密钥,选择新后端需要的 scope。
- 将新密钥部署到业务后端,使用原 subject 验证所需读写流程。
- 确认所有调用方已切换,再对旧前缀选择撤销密钥 → 确认撤销。
- 检查旧密钥的新请求被拒绝。若需要减少 scope,也应新建密钥;轮换不会自动缩小旧权限。
撤销影响后续鉴权请求,不等同于取消正在执行的任务。已有 SSE 连接也不是每条事件都重新做密钥鉴权;客户端更换密钥后应主动断开、用新密钥重连。单个任务的停止使用明确的 cancel,完整停用应用是影响整个应用的另一项管理操作。
业务凭据不能管理 Gateway#
当前 V1 业务入口供后端访问:带浏览器 Origin,或跨站 Sec-Fetch-Site 的请求会被拒绝。应由已鉴权的业务后端调用,不把 App Key 发给浏览器。
密钥不放 URL;token、key、ticket 这类查询参数会被拒绝。Onevium 页面使用的 /api/integrations/cloud/... 是桌面管理桥接地址,不是这里的公开 Gateway API。Node Gateway 的公开 /api/v1/management/** 返回 404,不能给 App Key 增加 scope 来打开它。
远端管理使用独立私有控制通道和令牌;设备连接使用设备身份;Webhook 使用签名。不要混用这几类凭据,远端配置见远端网关。
错误与验收重点#
| 现象 | 先检查什么 |
|---|---|
401 UNAUTHENTICATED | Bearer 格式、正确环境的密钥、是否轮换/撤销、应用是否仍有效 |
403 FORBIDDEN | 所需 scope、owner/设备状态,以及是否错误地从浏览器直连 |
400 INVALID_REQUEST | subject 缺失/无效、URL 放了凭据、请求字段不符合接口 |
| 已知 ID 返回 404 | 应用、subject、设备路由和实际 ID 是否匹配 |
| 轮换后仍有旧流输出 | 关闭旧 SSE,使用新密钥重连;不要把流输出当成新鉴权成功 |
真实环境还应检查:最小 scope、跨应用/subject 隔离、密钥轮换后的调用方切换、旧密钥拒绝、设备离线及恢复,以及需要的 HTTPS/SSE/回调路径。
下一步#
回到发布与首次请求,实现业务会话,或配置Webhook 和结果回调。