Token 实现指南(开发者视角)
读者画像:在仓内贡献代码的全栈/后端工程师,写新端点时想知道"挑哪个认证中间件 / 凭证怎么撤销 / 401 vs 403 vs 429 怎么区分"。
1. 视角说明(开篇必读)
本文档与 /api/authentication 互补,不冲突——两者按不同维度切分同一套认证体系:
| 文档 | 切分维度 | 分类 |
|---|---|---|
/api/authentication | 端点适配(端点接受哪种 Authorization) | JWT / API Key / Schedule / OAuth |
本文(/dev/token-types) | 凭证主体生命周期(颁发 / 校验 / 撤销) | Supabase JWT / Agent Token / API Key / OAuth (access + refresh) |
Schedule Token 与 Internal Secret 不属于独立"凭证"主体——前者由系统派发给定时任务、后者用于仓内服务间通信。但开发者写新端点时仍可能遇到,故在 §3 末尾以"非主轴凭证"补充。
2. 四种凭证总览
| 凭证 | 主体 | 颁发流程入口 | 寿命 | 典型场景 | 不该用于 |
|---|---|---|---|---|---|
| Supabase JWT | User | Supabase Auth(@supabase/auth-js) | 1h(自动刷新) | 浏览器会话 | 长寿命服务调用 |
Agent Token(wna- 前缀) | Team Agent(agent_bindings) | 创建 Agent 时随响应一次性返回;后续补发 / 轮换走 POST /teams/:teamId/agents/:bindingId/issue-token(Team Owner) | 永久(手动吊销) | Agent / 长寿命设备 | 直接给最终用户使用 |
API Key(wn- 前缀) | Team / 用户 | POST /teams/:teamId/api-keys(Team Owner) | 可配置 expires_at + 软撤销 | 第三方系统集成 | 浏览器会话 |
OAuth Token(wno- access + refresh) | OAuth App + User | POST /oauth/token | access 1h / refresh 30d | 第三方应用代用户调用 | 仓内服务间通信 |
实现源码:
- 工具函数:
apps/backend/src/utils/agent-token.ts、apps/backend/src/utils/oauth.ts、apps/backend/src/utils/schedule-token.ts - 数据库迁移:
supabase/sql/20260313_agent_token_permanent.sql、supabase/migrations/20260321_oauth_authorization_server.sql、supabase/migrations/20260404_api_permission_groups.sql、supabase/migrations/20260404_permission_rpc_functions.sql
3. 中间件选型矩阵
写新端点时的中间件选择决策表:
| 端点性质 | 推荐中间件 | 文件 |
|---|---|---|
| 用户专属(个人凭证管理、设置) | jwtAuth(仅 JWT) | apps/backend/src/middleware/jwt-auth.ts |
| 通用业务接口(用户 + API Key 都能调) | combinedAuth | apps/backend/src/middleware/combined-auth.ts |
| 已发布应用的运行时(OAuth + API Key + scope 校验) | oauthAuth + oauthScopeGuard + apiKeyAuth | apps/backend/src/middleware/oauth-auth.ts、apps/backend/src/middleware/api-key-auth.ts |
| 运行记录读 / 事件 / resume / cancel(JWT / API Key / Agent / OAuth) | selectAuth + requireRunAccess | apps/backend/src/middleware/run-auth.ts |
| API Key 专属端点 | apiKeyAuth | apps/backend/src/middleware/api-key-auth.ts(内部支持 wn- / wna- 前缀分发,并兼容 Schedule JWT) |
选型决策建议
- 能用
combinedAuth就别单挂jwtAuth——前者覆盖后者的能力且不阻挡 API Key 集成 - 运行时(runs / events / resume / cancel)一律走
selectAuth + requireRunAccess——它会按 run 上下文统一校验四种凭证的访问权 - OAuth 端点必须
oauthScopeGuard——只校验认证身份不校验 scope 等于绕过授权 - 新端点默认不挂
apiKeyAuth单独使用,除非明确是已发布应用入口
非主轴凭证(写新端点请知悉)
- Schedule Token:仅在直接挂载
apiKeyAuth的运行时路由中生效,由apiKeyAuth的 Bearer JWT 分支调用verifyScheduleToken()识别(参见apps/backend/src/middleware/api-key-auth.ts:118-135);不经过combinedAuth,也不通过selectAuth + requireRunAccess的 HTTP 访问链暴露 - Internal Secret:仅在
apiKeyAuth中通过X-Internal-Secret + X-Internal-Api-Key-Id旁路处理,限仓内 release_api webhook 投递等内部调用使用;新端点默认不要复用此通道
4. 凭证撤销机制(按类型分述)
每种凭证的撤销字段不同,必须分别讨论。
4.1 API Key
- 机制:软删除
api_keys.deleted_at或软撤销api_keys.revoked_at,认证中间件(apps/backend/src/middleware/authenticate-api-key.ts)对两者都拒收 - 当前实现:前端通过 Supabase RPC
delete_api_key(api_key_id)直接调用(不经过 REST 路由),写入deleted_at - REST 撤销端点:当前不存在,待 F3 子任务补全(具体路径由 F3 plan 决定,建议保持 team 资源口径如
DELETE /teams/:teamId/api-keys/:apiKeyId,本文档不预先定路径)
4.2 Agent Token(区分三类操作!)
Agent 域内有 3 个看似接近、实际语义完全不同的操作,写代码时不要混淆:
| 操作 | 端点 / 实现 | 数据库写入 | 效果 |
|---|---|---|---|
| 吊销 Token(Agent 本体仍存在) | POST /teams/:teamId/agents/:bindingId/revoke-token(参见 apps/backend/src/routes/team-agents.ts:730-768) | agent_bindings.token_hash = null | 旧 Token 立即失效,Agent 仍可被重新签发 |
| 禁用发送权 | UPDATE 接口(无独立路由) | agent_bindings.can_send = false | Agent 还在、Token 还在,但运行时拒绝发送 |
| 删除 Agent | DELETE /teams/:teamId/agents/:bindingId(参见 apps/backend/src/routes/team-agents.ts:822-849) | agent_bindings.deleted_at = NOW() | 整个 Agent 软删除,所有挂在它上面的 Token 一并失效 |
| 补发 / 轮换 Token(覆盖旧) | POST /teams/:teamId/agents/:bindingId/issue-token | 原子替换 agent_bindings.token_hash | 老 Token 立即失效,明文新 Token 仅返回一次(创建 Agent 已包含一次明文返回,本端点用于错过或定期轮换) |
4.3 OAuth Token
- Token 级:
POST /oauth/revoke(RFC 7009),写入oauth_tokens.revoked_at = NOW()(实现见apps/backend/src/routes/oauth.ts) - App 级:
oauth_apps.disabled_at禁用整个 OAuth App,所有该 App 颁发的 access / refresh token 全部失效
4.4 Supabase JWT
- 无独立撤销端点,依赖 Supabase Auth 自身的 session 失效机制(用户登出 / refresh token 过期 / 后端通过 Admin API 强制登出)
4.5 Cache 与失效时延(容易踩坑)
读这一节区分两层不同的缓存语义,避免误以为撤销有 30s 延迟:
| 检查项 | 是否走 cache | 失效时延 |
|---|---|---|
凭证状态(token_hash、revoked_at、deleted_at、expires_at、oauth_tokens.revoked_at) | 直查 DB(中间件无 token cache) | 撤销立即生效 |
路由权限分组(permission_groups,参见 apps/backend/src/services/permission.service.ts) | tokenCache + permission_version 30s TTL | 配置变更最多 30s |
简单说:撤销凭证立即生效,调整路由权限分组配置最多 30s 后生效。两者互不影响。
5. 跨端凭证派发流程
场景:用户在 Web 登录后,需要为长寿命设备 / Agent 派发 Token 让另一端使用。
入口
UI 入口在 /o/:teamSlug/agents(团队 Agent 管理页),不是 Settings/Devices。前端服务封装在 apps/frontend/src/services/agentService.ts。
流程(时序)
关键点:创建 Agent 时 backend 一次性生成并直接返回明文 Token(参见
apps/backend/src/routes/team-agents.ts的POST /teams/:teamId/agents实现,约 440-478 行;前端入口apps/frontend/src/services/agentService.ts的agentService.create())。POST .../issue-token只在错过明文或主动轮换时使用,会覆盖已有token_hash,使旧明文立即失效。
Team Owner Web (agentService) Backend Agent Device
│ │ │ │
│ 1. 创建 Agent │ │ │
│─────────────────────▶│ │ │
│ │ POST /teams/:id/agents │
│ │─────────────────────▶│ │
│ │ │ INSERT agent_bindings │
│ │ │ 同时生成 wna- token │
│ │ │ 存 token_hash │
│ │ ◀── { agent, token } │ │
│ │ (明文仅返回一次!) │ │
│ 2. 复制到设备 │ │ │
│ ─────────────────── │ ──────────────────── │ ─────────────────────▶ │
│ │ │ │
│ │ │ Agent 用 Token 调 API │
│ │ │ ◀────────────────────────│
│ │ │ selectAuth + run-auth │
│ │ │ 校验 token_hash │
│ │ │ ─────────────────────▶ │
│ │ │ │
│ 3. 补发 / 轮换 │ │ │
│ (错过明文或定期轮换) │
│─────────────────────▶│ POST issue-token │ │
│ │─────────────────────▶│ 生成新 token, │
│ │ │ 原子覆盖 token_hash │
│ │ ◀── 新明文 token │ │
│ │ (旧 token 立即失效) │ │
│ │ │ │
│ 4. 吊销(可选) │ │ │
│─────────────────────▶│ POST revoke-token │ │
│ │─────────────────────▶│ token_hash = null │
│ │ │ (Agent 调用立即 401) │安全提醒
- 明文 Token 只在 create / issue-token 响应里出现一次,前端不能持久化、必须由用户立即复制到目标设备
- 错过明文 → 调
agentService.issueToken()补发,旧 Token 立即失效 - 任何进入日志 / 监控的位置都不应出现明文 Token,仅可输出
token_hash前缀
6. 错误码与排错
| HTTP | 含义 | 触发原因 | 实现位置 |
|---|---|---|---|
| 401 Unauthorized | 凭证无效 | 过期 / 已撤销 / 格式错 / token_hash 已置空 / Schedule JWT 校验失败 | 各 auth 中间件 |
| 403 Forbidden(权限类) | 凭证有效但无权限 | scope 缺失 / Team 不匹配 / Agent 无 can_send | OAuth scope guard、role 检查 |
403 API key credit limit exceeded | API Key 积分耗尽(usage_credits >= limit_credits) | apps/backend/src/middleware/authenticate-api-key.ts:138-142 | API Key 认证 |
| 429 Too Many Requests | 请求频率限流(RPS) | Rate limiter | rate-limit middleware(如已挂载)+ X-RateLimit-* headers |
403 vs 429 别混淆:403 是"积分余额耗尽"(账面问题,需要充值或扩限额);429 是"调用过快"(速率问题,等几秒重试或退避)。前者在响应体的 error 字段中明确说明
credit limit exceeded,后者在 headers 中携带Retry-After。
排错速查
| 客户端表现 | 后端响应示例 | 应做的事 |
|---|---|---|
| 401 | { "error": "Invalid authorization token" } | 检查 Token 是否被撤销 / 过期;若是 Agent,确认 revoke-token 是否被误调用 |
| 401 | { "error": "API key has expired" } | 重新签发 API Key(或调整 expires_at) |
| 403 | { "error": "API key credit limit exceeded" } | 调高 limit_credits 或等周期重置 |
| 403 | { "error": "insufficient_scope" } | OAuth Token 缺少所需 scope,重新申请授权 |
| 403 | { "error": "Only team owner can ..." } | 调用方不是 Team Owner,换账号或调整 Team 角色 |
| 429 | { "error": "Too Many Requests" } + Retry-After header | 实现退避重试,降低并发 |
参考
- 认证端点视角文档:
/api/authentication - OAuth 完整接入:
/api/oauth、/guide/oauth、/oauth/callback - 后端中间件:
apps/backend/src/middleware/jwt-auth.ts、apps/backend/src/middleware/api-key-auth.ts、apps/backend/src/middleware/oauth-auth.ts、apps/backend/src/middleware/combined-auth.ts、apps/backend/src/middleware/run-auth.ts - API Key 校验实现:
apps/backend/src/middleware/authenticate-api-key.ts - OAuth 路由:
apps/backend/src/routes/oauth.ts - Agent 路由:
apps/backend/src/routes/team-agents.ts - 前端 Agent 服务:
apps/frontend/src/services/agentService.ts - 权限缓存:
apps/backend/src/services/permission.service.ts
本文档面向仓内开发者。如果你是接入外脑的第三方系统,请参考
/integration/authentication。