设备授权(一键鉴权)

积木客户端一键鉴权流程契约——发起授权、用户确认、轮询取令牌、刷新与登出的完整端点说明。

设备授权(Device Authorization,即积木客户端的「一键鉴权」)让桌面端、CLI 等无法直接复用浏览器登录态的客户端, 通过「打开一个授权页 + 用户确认」的方式完成登录,不需要用户手动创建、复制、粘贴 sk- 令牌。

一句话理解:这跟智能电视上登录视频 App 的体验很像——电视上不方便打字输密码, App 会显示一个码,你拿手机扫一下或输入这个码确认"是我",电视就自动登录成功了。 积木客户端也是同理:客户端弹出一个链接或二维码,你在浏览器里点一下确认,客户端就自动拿到了登录凭证, 全程不需要你自己去创建、复制、粘贴任何密钥。
这套流程面向积木官方客户端。调用 /token、/refresh 需要携带官方分发的 client_id / client_secret, 该凭证对随发行的官方客户端固定,不面向第三方开发者开放。第三方接入请直接使用 鉴权与令牌 中的 sk- 令牌方式。

适用场景

简单说:如果你在做一个独立客户端(桌面 App、CLI 工具等),想让用户"点一下就登录",而不是让用户自己去创建令牌再粘贴进来,就用这套流程。

  • 适合:积木桌面端、CLI 等本身没有浏览器登录界面、或者不方便让用户手动输入令牌的客户端。
  • 不适合:网页前端(浏览器里本来就能直接登录)、或者第三方服务对接(直接用 sk- 令牌更简单,见 鉴权与令牌)。

流程总览

整体流程分两条线同时进行:客户端这边一直在"问"服务端"用户确认了吗",用户那边在浏览器里点一下"确认"。 两条线通过一个共享的授权码对上,一旦用户确认,客户端下一次"问"就能拿到登录凭证。具体七步:

  1. 客户端调用 POST /api/auth/device/code 发起授权,取得 device_code、user_code 与授权页地址。
  2. 客户端打开或展示授权页地址(verification_uri_complete)给用户。
  3. 用户在浏览器打开授权页 GET /authorize?code=...,登录后点击确认或拒绝。
  4. 客户端按返回的 interval 间隔轮询 POST /api/auth/device/token,直到取得令牌或收到终态错误。
  5. 轮询成功后,客户端保存 access_token(管理接口用)、refresh_token(刷新用)、llm_token(模型调用接口用)。
  6. access_token 快过期时,用 refresh_token 调用 POST /api/auth/device/refresh 换取新的一对令牌。
  7. 用户退出登录时,客户端调用 POST /api/auth/device/logout,服务端删除本次授权记录并使 llm_token 失效。

端点总览

方法路径说明认证方式
POST/api/auth/device/code发起授权,生成设备码与用户码无
GET/authorize?code=...授权页(浏览器打开,非 API)页面自身处理登录
POST/api/auth/device/login授权页内嵌登录无(需有效 user_code)
GET/api/auth/device/verify查询 user_code 当前状态无
POST/api/auth/device/approve用户确认授权登录会话 / access token
POST/api/auth/device/deny用户拒绝授权登录会话 / access token
POST/api/auth/device/token客户端轮询取令牌client_id + client_secret
POST/api/auth/device/refresh刷新 access_tokenclient_id + client_secret
POST/api/auth/device/logout退出登录,撤销本次授权Bearer {access_token}
POST /api/auth/device/code 只校验 client_id,不接收也不需要 client_secret。 client_secret 只在 POST /api/auth/device/token(轮询)与 POST /api/auth/device/refresh(刷新)中需要。 这是两个最容易混淆的点:发起授权不需要 secret,取令牌和刷新令牌才需要。

令牌与状态

三种令牌

一次授权会拿到三个不同用途的凭证,可以理解成:access_token 是"客户端自己的通行证"(用来问服务端"我的账号信息是什么"这类问题); refresh_token 是"续期通行证的钥匙"(通行证快过期时用它换一张新的,不用重新走一遍授权); llm_token 才是真正拿去调用 AI 模型接口的那个密钥,跟你在控制台手动创建的令牌是一回事。

令牌用途有效期
access_token调用管理接口(/api 前缀),如查询用户信息30 天,可用 refresh_token 刷新
refresh_token换取新的 access_token90 天,每次刷新滚动生成新值
llm_token调用模型调用接口(/v1、/v2 前缀),等价于一个 sk- 令牌长期有效,退出登录时删除

授权状态(user_code / device_code 共用)

状态说明
pending等待用户在授权页确认或拒绝
approved用户已确认,令牌已生成,客户端尚未成功轮询取走
denied用户已拒绝
used客户端已成功轮询取走令牌,本次 device_code 作废
expiredpending 状态超过 10 分钟未处理
invaliduser_code 不存在
10 分钟过期窗口只作用于 pending 状态。用户一旦确认或拒绝,授权记录不再受该窗口限制, 客户端稍晚才轮询也能正常取到结果。

1. 发起授权

客户端第一步调用,创建一条授权记录,返回设备码、用户码和授权页地址。 相当于客户端跟服务端说"我要登录,麻烦给我发个码"——服务端返回两个码:一个给客户端自己拿着轮询用(device_code), 一个给用户看、拿去授权页确认用(user_code)。

POST/api/auth/device/code

无需认证。

client_idstring必填

官方客户端分发的客户端标识

client_namestring

客户端显示名称,超过 100 字符会被截断

device_idstring

设备唯一标识,超过 100 字符会被截断;建议同一设备安装持久化固定值,用于后续登出时清理该设备的旧授权

curl https://api.jimu.chat/api/auth/device/code \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "<CLIENT_ID>",
    "client_name": "Jimu Desktop",
    "device_id": "desktop-8f4b9e2a"
  }'
响应200
{
  "success": true,
  "data": {
    "device_code": "9mS4Yv...(32位随机字符串)",
    "user_code": "PH8T-TCFV",
    "verification_uri": "https://api.jimu.chat/authorize",
    "verification_uri_complete": "https://api.jimu.chat/authorize?code=PH8T-TCFV",
    "expires_in": 600,
    "interval": 5
  }
}
device_codestring

客户端轮询时使用的设备码,不展示给用户

user_codestring

用户可读的授权码,展示给用户或拼进授权页链接

verification_uristring

授权页基础地址

verification_uri_completestring

已带上 user_code 的完整授权页地址,客户端可直接打开

expires_inint

user_code / device_code 的 pending 状态有效期,单位秒,当前为 600

intervalint

建议轮询间隔,单位秒,当前为 5

错误响应(client_id 无效):

{ "success": false, "message": "无效的客户端ID" }

行为说明

  • 同一 device_id 发起新的授权请求时,会清理该设备之前所有 pending 状态的旧记录,避免堆积。
  • 已授权、已拒绝、已使用的历史记录不受影响。

2. 授权页

用户在浏览器打开的页面,完成登录并确认或拒绝授权。这是页面地址,不是给客户端直接调用的 API。

GET /authorize?code={user_code}
codestring必填

发起授权接口返回的 user_code

页面内部依次调用以下接口,第三方开发者了解即可,不需要自己实现这个页面:

  • 打开时调用 GET /api/auth/device/verify 检查 user_code 是否仍有效。
  • 未登录时展示登录表单,调用下方的授权页内嵌登录接口完成登录。
  • 用户点击确认时调用 POST /api/auth/device/approve。
  • 用户点击拒绝时调用 POST /api/auth/device/deny。

授权页内嵌登录

供授权页使用的登录接口,不是客户端直接调用的接口。不要求图形验证码,但必须携带有效、未过期、 仍为 pending 状态的 user_code,作为图形验证码的替代防护。

POST/api/auth/device/login

无需认证。

user_codestring必填

当前授权页对应的用户授权码

usernamestring必填

用户名或邮箱

passwordstring必填

登录密码

响应200
{ "success": true, "message": "", "data": { "id": 1, "username": "alice" } }

账号开启两步验证时,改为返回:

{ "success": true, "data": { "require_2fa": true } }

错误响应:

{ "success": false, "message": "无效的授权码" }

3. 查询授权码状态

公开接口,无需登录,用于授权页或客户端检查 user_code 当前进展到哪一步。

GET/api/auth/device/verify
user_codestring必填

用户授权码

curl "https://api.jimu.chat/api/auth/device/verify?user_code=PH8T-TCFV"

各状态响应示例:

{ "success": true, "status": "pending", "message": "授权码有效" }
{ "success": false, "status": "approved", "message": "授权码已授权" }
{ "success": false, "status": "denied", "message": "授权码已被拒绝" }
{ "success": false, "status": "used", "message": "授权码已使用" }
{ "success": false, "status": "expired", "message": "授权码已过期" }
{ "success": false, "status": "invalid", "message": "无效的授权码" }

只有 pending 状态返回 success: true,其余状态均为 false(即使请求本身成功)。

4. 用户确认授权

登录用户点击「确认」时调用,通常由授权页触发,第三方客户端不需要直接调用。

POST/api/auth/device/approve

需要登录会话或 access_token:

Authorization: Bearer <登录会话 access token>
Jimu-Api-User: <用户 ID>
这里的 Authorization 是调用方当前登录用户的会话凭证,与设备授权流程产出的 access_token 是两个不同东西—— 确认授权发生在用户已经登录的浏览器会话里,此时设备授权流程还没有产出任何令牌。
user_codestring必填

用户授权码

curl https://api.jimu.chat/api/auth/device/approve \
  -H "Authorization: Bearer <当前登录用户的 ACCESS_TOKEN>" \
  -H "Jimu-Api-User: <USER_ID>" \
  -H "Content-Type: application/json" \
  -d '{"user_code": "PH8T-TCFV"}'
响应200
{ "success": true, "message": "授权成功" }

错误响应:

{ "success": false, "message": "无效的授权码" }
{ "success": false, "message": "授权码已过期" }
{ "success": false, "message": "授权处理失败: 授权码已被处理" }

服务端行为

确认成功后,服务端会:

  • 生成 access_token(30 天有效)与 refresh_token(90 天有效)。
  • 创建一个 llm_token,可直接用于调用模型调用接口。
  • 将授权记录状态从 pending 更新为 approved。
  • 清理同一用户在同一 device_id 下的旧授权记录及其关联的旧 llm_token。

5. 用户拒绝授权

登录用户点击「拒绝」时调用,通常由授权页触发。

POST/api/auth/device/deny

认证方式与「用户确认授权」相同(登录会话或 access token + Jimu-Api-User)。

user_codestring必填

用户授权码

curl https://api.jimu.chat/api/auth/device/deny \
  -H "Authorization: Bearer <当前登录用户的 ACCESS_TOKEN>" \
  -H "Jimu-Api-User: <USER_ID>" \
  -H "Content-Type: application/json" \
  -d '{"user_code": "PH8T-TCFV"}'
响应200
{ "success": true, "message": "已拒绝授权" }

错误响应:

{ "success": false, "message": "无效的授权码" }

拒绝后授权记录状态变为 denied,客户端轮询会收到 access_denied 错误。

6. 轮询获取令牌

客户端用 device_code 反复调用这个接口,直到用户在授权页完成操作。 "轮询"就是客户端每隔几秒钟问一次服务端"用户确认了吗",还没确认就再等一会儿接着问, 确认了(或者拒绝、过期了)就停下来,不需要用户手动点"我已经确认了,你可以继续了"这类按钮。

POST/api/auth/device/token

需要 client_id + client_secret(官方客户端凭证)。

device_codestring必填

发起授权接口返回的设备码

client_idstring必填

官方客户端分发的客户端标识

client_secretstring必填

官方客户端分发的客户端密钥,与 client_id 配对使用;第三方开发者不会拿到这对凭证

curl https://api.jimu.chat/api/auth/device/token \
  -H "Content-Type: application/json" \
  -d '{
    "device_code": "9mS4Yv...(32位随机字符串)",
    "client_id": "<CLIENT_ID>",
    "client_secret": "<CLIENT_SECRET>"
  }'

成功响应

用户已确认授权且本次是第一次成功轮询时返回:

响应200
{
  "success": true,
  "data": {
    "access_token": "...",
    "refresh_token": "...",
    "llm_token": "...",
    "token_type": "Bearer",
    "expires_in": 2592000,
    "user_id": 1,
    "username": "admin",
    "display_name": "管理员"
  }
}
access_tokenstring

调用管理接口用的令牌

refresh_tokenstring

用于刷新 access_token;历史数据可能为空字符串,客户端需容忍

llm_tokenstring

调用模型调用接口用的令牌,等价于一个 sk- 令牌

token_typestring

固定为 Bearer

expires_inint

access_token 剩余有效期,单位秒

user_idint

授权用户 ID

usernamestring

授权用户名

display_namestring

授权用户显示名

轮询过程中的响应(按状态)

等待用户操作,继续轮询:

{ "success": false, "error": "authorization_pending", "message": "等待用户授权" }

用户拒绝,停止轮询:

{ "success": false, "error": "access_denied", "message": "用户拒绝了授权" }

pending 超过 10 分钟过期,停止轮询并重新发起授权:

{ "success": false, "error": "expired_token", "message": "授权码已过期" }

device_code 无效,或授权码已被取走过一次,停止轮询并重新发起授权:

{ "success": false, "error": "invalid_grant", "message": "无效的设备码" }
{ "success": false, "error": "invalid_grant", "message": "授权码已使用" }

client_id / client_secret 错误:

{ "success": false, "error": "invalid_client", "message": "无效的客户端凭证" }

服务端数据异常(罕见):

{ "success": false, "error": "server_error", "message": "授权记录不完整" }

轮询规则

  • 首次轮询间隔使用发起授权响应中的 interval(当前 5 秒)。
  • 收到 authorization_pending:按间隔继续轮询。
  • 收到 access_denied / expired_token / invalid_grant:停止轮询,这些都是终态,需要重新发起授权。
  • 收到 invalid_client:停止轮询,检查客户端凭证配置。
  • 成功取到令牌后,授权记录状态从 approved 变为 used;同一个 device_code 不能再取第二次。

轮询伪代码

interval = code_response.interval
loop until timeout:
  sleep(interval)
  res = POST /api/auth/device/token
  if res.success:
    save access_token, refresh_token, llm_token
    break
  if res.error == "authorization_pending":
    continue
  # access_denied / expired_token / invalid_grant / invalid_client:终止轮询
  show res.message
  break

7. 刷新令牌

access_token 快过期或已过期时,用 refresh_token 换取新的一对令牌。llm_token 不受影响。

POST/api/auth/device/refresh

需要 client_id + client_secret(官方客户端凭证)。

refresh_tokenstring必填

当前持有的刷新令牌

client_idstring必填

官方客户端分发的客户端标识

client_secretstring必填

官方客户端分发的客户端密钥

curl https://api.jimu.chat/api/auth/device/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "<旧的 REFRESH_TOKEN>",
    "client_id": "<CLIENT_ID>",
    "client_secret": "<CLIENT_SECRET>"
  }'
响应200
{
  "success": true,
  "data": {
    "access_token": "新的 access_token",
    "refresh_token": "新的 refresh_token",
    "token_type": "Bearer",
    "expires_in": 2592000
  }
}

错误响应:

{ "success": false, "error": "invalid_client", "message": "无效的客户端凭证" }
{ "success": false, "error": "invalid_grant", "message": "无效的 refresh_token" }
{ "success": false, "error": "invalid_grant", "message": "refresh_token 已过期,请重新授权" }
{ "success": false, "error": "invalid_grant", "message": "refresh_token 已被刷新或失效,请重试" }

刷新规则

  • refresh_token 有效期 90 天;每次刷新都会同时滚动生成新的 access_token 与新的 refresh_token。
  • 客户端必须用返回的新 refresh_token 覆盖旧值——旧的 refresh_token 用过一次即失效。
  • llm_token 不参与刷新,刷新前后保持不变。
  • refresh_token 失效或过期后,无法再刷新,需要重新走一次完整的设备授权流程。

8. 登出

客户端退出登录时调用,服务端撤销本次设备授权并使关联的 llm_token 失效。

POST/api/auth/device/logout
Authorization: Bearer <本次设备授权产出的 access_token>

无请求体。

curl -X POST https://api.jimu.chat/api/auth/device/logout \
  -H "Authorization: Bearer <ACCESS_TOKEN>"
响应200
{ "success": true, "message": "退出登录成功" }

错误响应:

{ "success": false, "message": "未提供令牌" }
{ "success": false, "message": "未找到授权记录" }

登出行为

  • 按 access_token 找到对应的设备授权记录并删除。
  • 删除该记录关联的 llm_token(真实令牌记录),并清理缓存,避免删除后仍在短时间内可用。
  • 登出后,本次授权拿到的 access_token、refresh_token、llm_token 全部失效,客户端应同步清除本地保存的这三个值。

9. 用 access_token 调管理接口

GET /api/user/self
Authorization: Bearer <access_token>
Jimu-Api-User: <user_id>

用法与普通登录会话换到的 access token 一致,详见 鉴权与令牌。 access_token 过期或失效时,管理接口返回 success: false,客户端应尝试用 refresh_token 刷新。

10. 用 llm_token 调模型调用接口

llm_token 是设备授权自动创建的真实令牌,用法与 sk- 令牌完全一致:

curl https://api.jimu.chat/v1/chat/completions \
  -H "Authorization: Bearer <llm_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<模型 ID>",
    "messages": [{"role": "user", "content": "Hello"}]
  }'
  • llm_token 使用当前用户的额度与套餐权限,行为与用户在控制台自建的令牌相同。
  • llm_token 不随 access_token 刷新而变化,是长期有效的独立令牌。
  • 调用登出接口后,llm_token 立即失效。

错误码汇总

error出现在哪个接口含义客户端处理建议
invalid_requesttoken、refresh请求参数缺失或格式错误检查请求体字段
invalid_clienttoken、refreshclient_id / client_secret 错误检查客户端凭证配置
authorization_pendingtoken用户尚未确认或拒绝按间隔继续轮询
access_deniedtoken用户已拒绝授权停止轮询,提示用户,可重新发起
expired_tokentokenpending 授权码已过期(超过 10 分钟)停止轮询,重新发起授权
invalid_granttoken、refresh设备码/刷新令牌无效、已被使用或状态异常重新发起授权或提示用户重新登录
server_errortoken服务端数据异常提示稍后重试

客户端保存建议

  • device_id 建议在设备本地持久化,同一台设备的多次安装/重装保持不变,便于登出与旧授权清理。
  • access_token、refresh_token、llm_token 应存入系统安全存储(如系统密钥链),不要写入普通日志。
  • refresh_token 每次刷新成功后必须覆盖旧值再保存。
  • 管理接口返回过期提示时,先尝试用 refresh_token 刷新;刷新也失败时清除本地令牌并重新发起授权。
  • 模型调用接口返回鉴权失败或用户主动登出后,应清除本地保存的三个令牌。