认证与 key 安全
平台有两套互不相通的认证,搞清楚它们就不会踩坑:
| 平面 | 凭据 | 用在哪 | 怎么来 |
|---|---|---|---|
| 机器侧 | Authorization: Bearer sk-meow-… | /v1/* 全部 API | 控制台签发 |
| 人侧 | mb_session httpOnly cookie | 控制台页面与 /console/*、/auth/me | 登录自动种下 |
API key 不能用来登录控制台,cookie 也不能用来调 /v1/*。
Bearer key 怎么用
每个 API 请求都带上请求头:
Authorization: Bearer sk-meow-xxxxxxxx…- key 形状:
sk-meow-前缀 + 64 位十六进制(256 bit 熵); - 服务端校验方式:对收到的 key 做 sha256 后按哈希等值查找——比对对象是哈希而非明文,对 key 本身的计时探测无效;控制台会话 cookie 的 HMAC 验签才是常量时间比较;
- 鉴权失败一律返回 401「未授权」,刻意不区分「key 不存在 / 已撤销 / 格式错」——防探测。
安全纪律(平台侧承诺)
- 明文绝不入库:数据库只存 key 的 sha256 哈希与前 8 位前缀(用于列表展示);
- 明文只显示一次:签发响应是唯一一次看到明文的机会,之后任何接口都不再返回;
- 不进日志:key 不写入服务端日志,错误信息经统一脱敏处理。
安全纪律(你这一侧的建议)
- key 只放
Authorization请求头,不要拼进 URL、不要写进前端源码、不要提交进 git; - 浏览器端直连
/v1/agent仅适合自己调试(如控制台试聊台);对外产品请由你的后端代持 key; - 按用途分 key(一个应用一把),泄露时只撤销受影响的那把;
- 撤销立即生效:被撤销的 key 下一个请求就是 401。
控制台会话(人侧)
- 登录后种下
mb_sessioncookie:HttpOnly; Secure; SameSite=Lax,7 天有效; - 会话是无状态签名 token(HMAC-SHA256),服务端不存会话表;
- 登出 ≠ 吊销:登出只是让浏览器丢弃 cookie;token 本身到 7 天过期前在密码学上仍有效。这是设计取舍——请像保管密码一样保管你的浏览器会话。
原生 App 模式(GitHub OAuth,ADR-0025)
上面两套认证都是给浏览器用的:控制台页面跑在浏览器里,cookie 能自动种下。但原生 App(手机端用系统浏览器 Custom Tab 打开登录页)拿不到这个 cookie,也跳不回 App 里——所以 GitHub 登录额外有一条「app 模式」,让 App 也能登 GitHub 换到一把 API key。
只有 GitHub 登录需要这条。邮箱 OTP 登录原生可以直接用——
POST /auth/email/start发码、POST /auth/email/verify验码,验码响应直接带回mb_session会话 cookie(原生 HTTP 客户端自己就能收下、不像 Custom Tab 那样拿不到),App 持这个会话再POST /console/keys签一把 API key 即可。整条都是普通 HTTP 请求,不依赖浏览器跳转,所以不需要 app 模式这条深链流程。
三步换 key
-
App 本地先生成一对 PKCE 值:一个高熵随机串
code_verifier(自己保存,别发出去),和它的「指纹」code_challenge = base64url(sha256(code_verifier))。 -
用系统浏览器(Custom Tab)打开:
codeGET /auth/github?app=1&code_challenge=<上面的 challenge>&code_challenge_method=S256用户在 GitHub 授权页点同意后,云端不种 cookie,而是 302 跳一个深链回你的 App:
codemeowbot://auth/callback?code=<一次性 code>(
meowbot://是 App 约定的自定义 scheme,App 注册它来接收回跳。) -
App 拿 code + 当初的 verifier 换 key:
codePOST /auth/app/exchange Content-Type: application/json { "code": "<深链带回的 code>", "code_verifier": "<第 1 步保存的 verifier>" }成功返回:
json{ "api_key": "sk-meow-…", "tenant_id": "…" }这把
api_key就是机器侧 Bearer key,之后照常Authorization: Bearer sk-meow-…调/v1/*。明文只在这一次返回,务必当场存好。
为什么要 PKCE
深链(meowbot://…)在手机上理论上可能被同机装的别的 app 抢注截获。光有 code 不够安全——所以换 key 时必须同时交出第 1 步那个只有你自己知道的 code_verifier,云端验证 sha256(verifier) 和当初发起时存的 code_challenge 对得上,才发 key。这样即使 code 被别人截走,没有 verifier 也换不到 key。这是 OAuth 原生应用的标准做法(PKCE,RFC 7636,本平台只接受 S256,不接受 plain)。
一次性 code 的纪律
- 单次:code 一旦成功换过 key 就立即作废,重放同一个 code 直接 401;
- 短命:code 只在 KV 里存 5 分钟,过期即 401;
- 失败一律 401 且不告诉你具体原因(code 不存在 / 过期 / 已用 / verifier 不符,措辞都一样)——刻意脱敏,防探测;
code/code_verifier/ 换到的 key 都不写进服务端日志。
常见错误码
| 码 | 含义 | 处理 |
|---|---|---|
| 401 | key 缺失 / 无效 / 已撤销 | 检查 Authorization 头;必要时重签 key |
| 429 | 触发该 key 的限流 | 读 Retry-After 头(秒),等够再发;详见限流与用量 |