设备授权(一键鉴权)
积木客户端一键鉴权流程契约——发起授权、用户确认、轮询取令牌、刷新与登出的完整端点说明。
设备授权(Device Authorization,即积木客户端的「一键鉴权」)让桌面端、CLI 等无法直接复用浏览器登录态的客户端,
通过「打开一个授权页 + 用户确认」的方式完成登录,不需要用户手动创建、复制、粘贴 sk- 令牌。
/token、/refresh 需要携带官方分发的 client_id / client_secret,
该凭证对随发行的官方客户端固定,不面向第三方开发者开放。第三方接入请直接使用
鉴权与令牌 中的 sk- 令牌方式。适用场景
简单说:如果你在做一个独立客户端(桌面 App、CLI 工具等),想让用户"点一下就登录",而不是让用户自己去创建令牌再粘贴进来,就用这套流程。
- 适合:积木桌面端、CLI 等本身没有浏览器登录界面、或者不方便让用户手动输入令牌的客户端。
- 不适合:网页前端(浏览器里本来就能直接登录)、或者第三方服务对接(直接用
sk-令牌更简单,见 鉴权与令牌)。
流程总览
整体流程分两条线同时进行:客户端这边一直在"问"服务端"用户确认了吗",用户那边在浏览器里点一下"确认"。 两条线通过一个共享的授权码对上,一旦用户确认,客户端下一次"问"就能拿到登录凭证。具体七步:
- 客户端调用
POST /api/auth/device/code发起授权,取得device_code、user_code与授权页地址。 - 客户端打开或展示授权页地址(
verification_uri_complete)给用户。 - 用户在浏览器打开授权页
GET /authorize?code=...,登录后点击确认或拒绝。 - 客户端按返回的
interval间隔轮询POST /api/auth/device/token,直到取得令牌或收到终态错误。 - 轮询成功后,客户端保存
access_token(管理接口用)、refresh_token(刷新用)、llm_token(模型调用接口用)。 access_token快过期时,用refresh_token调用POST /api/auth/device/refresh换取新的一对令牌。- 用户退出登录时,客户端调用
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_token | client_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_token | 90 天,每次刷新滚动生成新值 |
llm_token | 调用模型调用接口(/v1、/v2 前缀),等价于一个 sk- 令牌 | 长期有效,退出登录时删除 |
授权状态(user_code / device_code 共用)
| 状态 | 说明 |
|---|---|
pending | 等待用户在授权页确认或拒绝 |
approved | 用户已确认,令牌已生成,客户端尚未成功轮询取走 |
denied | 用户已拒绝 |
used | 客户端已成功轮询取走令牌,本次 device_code 作废 |
expired | pending 状态超过 10 分钟未处理 |
invalid | user_code 不存在 |
pending 状态。用户一旦确认或拒绝,授权记录不再受该窗口限制,
客户端稍晚才轮询也能正常取到结果。1. 发起授权
客户端第一步调用,创建一条授权记录,返回设备码、用户码和授权页地址。
相当于客户端跟服务端说"我要登录,麻烦给我发个码"——服务端返回两个码:一个给客户端自己拿着轮询用(device_code),
一个给用户看、拿去授权页确认用(user_code)。
/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"
}'
{
"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_inintuser_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,作为图形验证码的替代防护。
/api/auth/device/login无需认证。
user_codestring必填当前授权页对应的用户授权码
usernamestring必填用户名或邮箱
passwordstring必填登录密码
{ "success": true, "message": "", "data": { "id": 1, "username": "alice" } }
账号开启两步验证时,改为返回:
{ "success": true, "data": { "require_2fa": true } }
错误响应:
{ "success": false, "message": "无效的授权码" }
3. 查询授权码状态
公开接口,无需登录,用于授权页或客户端检查 user_code 当前进展到哪一步。
/api/auth/device/verifyuser_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. 用户确认授权
登录用户点击「确认」时调用,通常由授权页触发,第三方客户端不需要直接调用。
/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"}'
{ "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. 用户拒绝授权
登录用户点击「拒绝」时调用,通常由授权页触发。
/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"}'
{ "success": true, "message": "已拒绝授权" }
错误响应:
{ "success": false, "message": "无效的授权码" }
拒绝后授权记录状态变为 denied,客户端轮询会收到 access_denied 错误。
6. 轮询获取令牌
客户端用 device_code 反复调用这个接口,直到用户在授权页完成操作。
"轮询"就是客户端每隔几秒钟问一次服务端"用户确认了吗",还没确认就再等一会儿接着问,
确认了(或者拒绝、过期了)就停下来,不需要用户手动点"我已经确认了,你可以继续了"这类按钮。
/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>"
}'
成功响应
用户已确认授权且本次是第一次成功轮询时返回:
{
"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_inintaccess_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 不受影响。
/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>"
}'
{
"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 失效。
/api/auth/device/logoutAuthorization: Bearer <本次设备授权产出的 access_token>
无请求体。
curl -X POST https://api.jimu.chat/api/auth/device/logout \
-H "Authorization: Bearer <ACCESS_TOKEN>"
{ "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_request | token、refresh | 请求参数缺失或格式错误 | 检查请求体字段 |
invalid_client | token、refresh | client_id / client_secret 错误 | 检查客户端凭证配置 |
authorization_pending | token | 用户尚未确认或拒绝 | 按间隔继续轮询 |
access_denied | token | 用户已拒绝授权 | 停止轮询,提示用户,可重新发起 |
expired_token | token | pending 授权码已过期(超过 10 分钟) | 停止轮询,重新发起授权 |
invalid_grant | token、refresh | 设备码/刷新令牌无效、已被使用或状态异常 | 重新发起授权或提示用户重新登录 |
server_error | token | 服务端数据异常 | 提示稍后重试 |
客户端保存建议
device_id建议在设备本地持久化,同一台设备的多次安装/重装保持不变,便于登出与旧授权清理。access_token、refresh_token、llm_token应存入系统安全存储(如系统密钥链),不要写入普通日志。refresh_token每次刷新成功后必须覆盖旧值再保存。- 管理接口返回过期提示时,先尝试用
refresh_token刷新;刷新也失败时清除本地令牌并重新发起授权。 - 模型调用接口返回鉴权失败或用户主动登出后,应清除本地保存的三个令牌。