32 KiB
TalkingQ 硬件端注册、临时 Token、OTA 全流程接口文档
版本:2026-05-08
适用对象:硬件固件、产测工具、工厂烧录工具开发同事
代码依据:gateway、register、token、ota 当前源码
1. 总览
默认统一从 Gateway 调用:
http://<gateway-host>:8090
不建议设备或工厂工具直连下游服务端口。Gateway 会统一做外部鉴权、限流、单飞控制,并自动给下游服务补充内部调用签名 X-Internal-*。
下游服务端口仅用于定位问题:
| 服务 | 默认端口 | Gateway 路由前缀 |
|---|---|---|
| gateway | 8090 | - |
| register | 8097 | /register/** |
| token | 8095 | /token/** |
| ota | 8096 | /ota/** |
2. 全流程逻辑示意图
flowchart TD
A[工厂产测/烧录工具] --> B{是否已有设备记录}
B -->|无| C[调用 /register/api/v1/devices 或 /batch 创建设备]
C --> C1[Gateway X-Factory-* HMAC 鉴权]
C1 --> C2[Register 校验 X-Api-Key 是否启用/配额是否足够]
C2 --> D[写入 sys_device 和 device_app 配置]
B -->|有| E[设备生成 P-256 密钥对]
D --> E
E --> F[调用 /register/api/auth/challenge]
F --> F1[首绑/重绑阶段需要 station_id + sig_b64 工位私钥签名]
F1 --> G[服务返回 challenge_id/challenge]
G --> H[设备用设备私钥签名 challenge 消息]
H --> I[调用 /register/api/auth/response]
I --> I1[服务验签后签发设备 JWT access_token]
I1 --> J[设备携带设备 JWT 调用 /token/api/device/token]
J --> K[Token 服务返回阿里云 DashScope 临时 token/appId/workspaceId/voiceId]
K --> L[设备连接阿里云 WebSocket/语音链路]
L --> M[设备定期调用 /ota/api/ota/versionInfoReport 上报固件版本]
M --> N{是否需要升级}
N -->|否| O[HTTP 204 No Content]
N -->|是| P[返回最新 version/url/sha256/sizeBytes/force/update]
P --> Q[设备下载固件并校验 SHA-256]
Q --> R[设备升级重启后再次 versionInfoReport]
鉴权分层:
flowchart LR
U[外部调用方] --> G[Gateway]
G -->|/register/api/v1/**| FH[X-Factory-* HMAC + X-Api-Key]
G -->|/token/** /ota/api/ota/**| DJ[设备 JWT ES256 + scope]
G -->|/ota/api/program/**| PJ[小程序设备 JWT HS256, type=device]
G -->|转发到下游| IH[Gateway 自动注入 X-Internal-*]
FH --> R[register]
DJ --> T[token/ota]
PJ --> O[ota program]
IH --> R
IH --> T
3. 通用约定
| 项 | 说明 |
|---|---|
| Content-Type | JSON 接口统一使用 application/json |
| 时间戳 | 除特别说明外,均为 Unix 秒级时间戳 |
| Base64 | sig_b64 使用标准 Base64;设备公钥、challenge、设备签名使用 base64url 且无 padding |
| Authorization | 设备 JWT 调用格式:Authorization: Bearer <access_token> |
| 成功响应包装 | AjaxResult/ResultMessage 通常为 {"code":200,"message":"...","data":...} |
| 业务错误包装 | Register 认证错误通常为 {"error_code":"...","message":"..."};Gateway 鉴权失败通常为 HTTP 401/403 |
4. Gateway 工厂 HMAC 鉴权
当前代码中 Gateway 工厂鉴权只拦截:
/register/api/v1/**
也就是以下设备注册接口必须带 X-Factory-*:
GET /register/api/v1/devices/{deviceId}
POST /register/api/v1/devices
POST /register/api/v1/devices/batch
/register/api/factory/** 当前不在 Gateway factory-auth.include-paths 内,这类接口主要依赖请求体里的工位私钥签名 sig_b64。如果未来希望它们也经过 Gateway HMAC,需要把 Gateway 配置里的 gateway.factory-auth.include-paths 扩展到 /register/api/factory/**。
4.1 必填请求头
| Header | 必填 | 说明 |
|---|---|---|
X-Api-Key |
是 | 工厂注册 API Key;Gateway 查 Redis 是否启用,Register 查数据库额度 |
X-Factory-Timestamp |
是 | Unix 秒级时间戳,默认允许偏移 300 秒 |
X-Factory-Nonce |
是 | 每次请求唯一随机串,默认 300 秒内不可重复 |
X-Factory-Signature |
是 | HMAC-SHA256 签名后标准 Base64 |
4.2 待签名 payload
Gateway 按下面 6 行拼接,换行符是 \n:
<HTTP_METHOD>
<RAW_PATH>
<RAW_QUERY>
<BODY_SHA256_HEX>
<X-Factory-Timestamp>
<X-Factory-Nonce>
字段规则:
| 字段 | 说明 |
|---|---|
HTTP_METHOD |
大写方法名,例如 POST |
RAW_PATH |
原始路径,例如 /register/api/v1/devices |
RAW_QUERY |
原始 query,不含 ?;没有 query 时为空字符串 |
BODY_SHA256_HEX |
请求体字节的 SHA-256 小写 hex;请求体为空时是空字符串,不是空 body 的 SHA-256 |
timestamp |
与请求头完全一致 |
nonce |
与请求头完全一致 |
签名计算:
X-Factory-Signature = Base64( HMAC-SHA256(FACTORY_AUTH_SECRET, payload_utf8) )
注意:
| 项 | 说明 |
|---|---|
FACTORY_AUTH_SECRET |
后端环境变量 FACTORY_AUTH_SECRET,必须由服务端安全下发给工厂工具,不应写死在固件里 |
| Redis 启用状态 | Gateway 会检查 gateway:factory:api-key:enabled:{apiKey} |
| DB 启用/配额 | Register 会检查 factory_register_quota 是否启用、未过期、额度足够 |
| 防重放 | 同一 apiKey + nonce 默认 300 秒内只能使用一次 |
4.3 Python 生成 Gateway 签名示例
import base64
import hashlib
import hmac
import json
import time
import uuid
api_key = "factory-api-key"
secret = "factory-auth-secret"
method = "POST"
raw_path = "/register/api/v1/devices"
raw_query = ""
body_obj = {
"deviceId": "550e8400-e29b-41d4-a716-446655440000",
"deviceName": "0508-0001",
"userId": 1,
"appId": "mm_xxx",
"workspaceId": "llm_xxx",
"voiceId": "kenny",
"product": "TQ_cat",
"version": "1.0.0"
}
body = json.dumps(body_obj, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time()))
nonce = uuid.uuid4().hex
body_hash = hashlib.sha256(body).hexdigest() if body else ""
payload = "\n".join([method, raw_path, raw_query, body_hash, timestamp, nonce])
signature = base64.b64encode(
hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).digest()
).decode("ascii")
headers = {
"Content-Type": "application/json",
"X-Api-Key": api_key,
"X-Factory-Timestamp": timestamp,
"X-Factory-Nonce": nonce,
"X-Factory-Signature": signature,
}
5. 工位私钥签名 sig_b64
工位签名用于工厂刷写/解锁/上报,以及设备首次绑定或强制重绑时证明“这台设备是在合法工位上生产/绑定”。
涉及接口:
POST /register/api/factory/flash-key
POST /register/api/factory/unlock
POST /register/api/factory/report
POST /register/api/auth/challenge 首绑/重绑或 pending 阶段需要
POST /register/api/auth/response pending 阶段需要
5.1 工位密钥配置
Register 服务通过环境变量 DEVICE_AUTH_FACTORY_STATION_SECRETS_PATH 指定工位公钥 JSON 文件。支持如下形式:
{
"station-001": {
"factory_id": "factory-a",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}
}
也支持 public_key、public_key_pem_b64、public_key_b64 字段。
5.2 待签名 JSON
服务端校验规则:
- 从请求体中移除
sig_b64。 - 对剩余 JSON 按 key 排序序列化。
- 使用工位公钥验签。
- 校验
timestamp默认 300 秒时间窗。 - 将
station_id + nonce写入数据库防重放。
推荐工厂工具按如下规则生成待签名 bytes:
canonical = json.dumps(payload_without_sig, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")
签名算法按工位公钥类型自动选择:
| 工位公钥类型 | 服务端验签算法 | sig_b64 格式 |
|---|---|---|
| EC | SHA256withECDSA |
DER ECDSA 签名字节再标准 Base64 |
| RSA | RSASSA-PSS + SHA-256 |
签名字节标准 Base64 |
| DSA | SHA256withDSA |
签名字节标准 Base64 |
| Ed25519/Ed448 | Ed25519/Ed448 |
签名字节标准 Base64 |
如果工位使用 P-256 EC 私钥,sig_b64 必须是 DER ECDSA 签名的标准 Base64,不是 raw r||s。
5.3 Python 生成工位签名示例
import base64
import json
import time
import uuid
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
station_private_key_pem = b"""-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----"""
private_key = serialization.load_pem_private_key(station_private_key_pem, password=None)
payload = {
"station_id": "station-001",
"timestamp": int(time.time()),
"nonce": uuid.uuid4().hex,
"device_id": "550e8400-e29b-41d4-a716-446655440000"
}
canonical = json.dumps(payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")
sig_der = private_key.sign(canonical, ec.ECDSA(hashes.SHA256()))
payload["sig_b64"] = base64.b64encode(sig_der).decode("ascii")
6. 设备 P-256 认证协议
设备认证成功后,Register 服务签发 ES256 设备 JWT。该 JWT 后续用于调用 Token 和 OTA 设备接口。
6.1 设备公钥 pub_key
设备使用 P-256,也就是 secp256r1。pub_key 格式:
base64url_no_padding( 0x04 || X[32] || Y[32] )
其中 X、Y 是 P-256 公钥坐标,固定 32 字节大端。
6.2 公钥指纹 pub_key_jkt
服务端按 JWK thumbprint 思路计算:
{"crv":"P-256","kty":"EC","x":"<base64url(X)>","y":"<base64url(Y)>"}
然后:
pub_key_jkt = base64url_no_padding( SHA256( compact_sorted_jwk_json_utf8 ) )
6.3 challenge 签名消息
设备调用 /register/api/auth/challenge 后,服务返回 challenge_id 和 challenge。设备用设备私钥签名下面的原始字节:
UTF8("robot-auth-v1")
+ UUID_TO_16_BYTES(device_id)
+ base64url_decode(challenge_id)
+ base64url_decode(challenge)
+ base64url_decode(pub_key_jkt)
+ int32_be(key_version)
签名算法:P-256 ECDSA + SHA-256。
提交给服务端的 signature 格式:
base64url_no_padding( raw_r_32 || raw_s_32 )
注意这里和工位签名不同:设备签名要求 raw r||s,不是 DER。
6.4 rotate 签名消息
密钥轮换时,设备用当前私钥签名:
UTF8("robot-rotate-v1")
+ UUID_TO_16_BYTES(device_id)
+ int32_be(current_key_version)
+ base64url_decode(new_pub_key_jkt)
+ base64url_decode(new_pub_key)
提交格式仍是:
base64url_no_padding( raw_r_32 || raw_s_32 )
6.5 设备 JWT 内容
Register 签发的 JWT:
| Claim | 说明 |
|---|---|
sub |
device_id |
exp |
默认当前时间 + 600 秒 |
scope |
默认 ota:report ota:read ota:confirm voice:session |
kv |
当前设备密钥版本 |
Gateway 对 /token/** 和 /ota/** 会校验:
- JWT ES256 签名正确。
exp未过期。- scope 命中接口规则。
- Redis 中
gateway:device:auth:{deviceId}的auth_status=active。 - JWT
kv等于当前key_version,或等于仍在宽限期的prev_key_version。
7. 推荐硬件调用顺序
7.1 工厂生产阶段
- 工厂工具生成设备 UUID
deviceId和序列号serialNumber。 - 调用
POST /register/api/v1/devices或/batch创建设备和默认应用配置。 - 设备生成 P-256 密钥对。
- 工厂工具或设备调用
POST /register/api/auth/challenge,首绑时带station_id/timestamp/nonce/sig_b64/pub_key/key_version。 - 设备签名 challenge 后调用
POST /register/api/auth/response获取设备 JWT。 - 设备保存私钥、
device_id、key_version。不要保存工厂 HMAC secret 或工位私钥到设备固件。
7.2 设备运行阶段
- 若没有有效设备 JWT,重新走 challenge/response。
- 携带设备 JWT 调用
POST /token/api/device/token获取阿里云临时 token。 - 使用返回的
token/appId/workspaceId/voiceId接入语音链路。 - 定期或开机调用
POST /ota/api/ota/versionInfoReport上报固件版本。 - 收到 204 表示无需升级;收到 200 且
data.notUptodate=1表示需要升级。 - 下载固件,校验
sha256和sizeBytes,升级后再次上报版本。
8. Register 接口
8.1 创建设备
POST /register/api/v1/devices
鉴权:
| 层级 | 要求 |
|---|---|
| Gateway | 必须通过 X-Factory-* HMAC |
| Register | 必须通过 X-Api-Key 数据库启用/配额校验 |
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
deviceId |
string | 是 | 设备 UUID,后续设备认证要求 UUID 格式 |
userId |
integer | 是 | 绑定用户 ID |
deviceName |
string | 否 | 设备名称/序列号;为空时服务端生成 MMdd |
batchId |
string | 否 | 批次 |
clientId |
integer | 否 | 客户/渠道 ID |
functionNames |
string | 否 | 功能描述 |
ip |
string | 否 | 工厂或设备 IP |
location |
string | 否 | 位置信息 |
wifiName |
string | 否 | Wi-Fi 名称 |
chipModelName |
string | 否 | 芯片型号 |
type |
string | 否 | 设备类型 |
version |
string | 否 | 初始固件版本 |
state |
string | 否 | 状态,默认 "1" |
product |
string | 否 | 产品标识,用于 OTA 版本匹配 |
appId |
string | 否 | 阿里云应用 ID;为空使用默认值 |
workspaceId |
string | 否 | 阿里云 workspace;为空使用默认值 |
voiceId |
string | 否 | 声音 ID;为空使用默认值 kenny |
成功响应:
{
"code": 200,
"message": "设备新建成功,app配置添加成功。",
"data": {
"deviceId": "550e8400-e29b-41d4-a716-446655440000",
"deviceName": "0508-0001",
"product": "TQ_cat",
"version": "1.0.0"
}
}
8.2 查询设备
GET /register/api/v1/devices/{deviceId}
鉴权同创建设备,必须带 X-Factory-* 和 X-Api-Key。
响应:
{
"code": 200,
"message": "success",
"data": {
"deviceId": "550e8400-e29b-41d4-a716-446655440000",
"deviceName": "0508-0001",
"product": "TQ_cat",
"version": "1.0.0"
}
}
8.3 批量创建设备
POST /register/api/v1/devices/batch
鉴权同创建设备。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
devices |
array | 是 | 设备列表 |
devices[].deviceId |
string | 是 | 设备 ID |
devices[].deviceName |
string | 否 | 设备名称 |
devices[].serialNumber |
string | 是 | 序列号,会写入 deviceName 和 device_app.serialNumber |
devices[].batchId |
string | 否 | 批次 |
devices[].clientId |
integer | 否 | 客户/渠道 ID |
devices[].product |
string | 否 | 产品标识 |
userId |
integer | 是 | 用户 ID |
appId |
string | 是 | 阿里云应用 ID |
workspaceId |
string | 是 | workspace |
voiceId |
string | 否 | 声音 ID |
state |
string | 否 | 状态,默认 "1" |
成功响应:
{
"code": 200,
"message": "操作成功",
"data": {
"createdCount": 2,
"appConfigCount": 2
}
}
8.4 工厂申请刷写密钥
POST /register/api/factory/flash-key
鉴权:请求体必须带工位私钥签名 sig_b64。当前 Gateway 不要求 X-Factory-*。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
station_id |
string | 是 | 工位 ID |
timestamp |
integer | 是 | Unix 秒 |
nonce |
string | 是 | 防重放随机串,最长 128 |
device_id |
string | 是 | 设备 ID,最长 128 |
sig_b64 |
string | 是 | 对移除 sig_b64 后的请求体做工位签名 |
成功响应:
| 字段 | 类型 | 说明 |
|---|---|---|
key_b64 |
string | 32 字节刷写密钥的标准 Base64 |
issued_at |
string | UTC ISO 时间 |
expires_at |
string | UTC ISO 过期时间 |
expires_in |
integer | 默认 600 秒 |
server_time |
integer | 服务端 Unix 秒 |
factory_id |
string | 工位配置中的工厂 ID,可能为空 |
station_id |
string | 工位 ID |
sig_b64 |
string | 服务端用工厂私钥签名响应 |
alg |
string | 响应签名算法,例如 ecdsa-sha256 |
8.5 工厂申请解锁凭据
POST /register/api/factory/unlock
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
station_id |
string | 是 | 工位 ID |
timestamp |
integer | 是 | Unix 秒 |
nonce |
string | 是 | 防重放随机串 |
device_mac |
string | 是 | MAC,格式 AA:BB:CC:DD:EE:FF |
purpose |
string | 是 | 默认必须为 factory_unlock |
proto_version |
integer | 是 | 默认必须为 1 |
sig_b64 |
string | 是 | 工位签名 |
成功响应包含:
{
"device_mac": "aa:bb:cc:dd:ee:ff",
"purpose": "factory_unlock",
"proto_version": 1,
"issued_at": "2026-05-08T01:00:00Z",
"expires_at": "2026-05-08T01:05:00Z",
"expires_in": 300,
"server_time": 1778192400,
"nonce": "base64url-random",
"factory_id": "factory-a",
"station_id": "station-001",
"sig_b64": "...",
"alg": "ecdsa-sha256"
}
8.6 工厂上报刷机/注册结果
POST /register/api/factory/report
请求体必填:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
station_id |
string | 是 | 工位 ID |
timestamp |
integer | 是 | Unix 秒 |
nonce |
string | 是 | 防重放随机串 |
device_id |
string | 是 | 设备 ID |
firmware_version |
string | 是 | 烧录固件版本 |
steps.flash.status |
string | 是 | 刷写步骤状态 |
steps.register.status |
string | 是 | 注册步骤状态 |
success |
boolean/integer | 否 | 是否成功 |
operator |
string | 否 | 操作员 |
sig_b64 |
string | 是 | 工位签名 |
成功响应:
{
"status": "ok",
"report_id": "uuid",
"server_time": 1778192400
}
8.7 发起设备认证挑战
POST /register/api/auth/challenge
鉴权:
| 场景 | 是否需要工位签名 |
|---|---|
设备未绑定,首次写入 pub_key |
是 |
| 设备 pending 首绑未完成 | 是 |
force_rebind=true 强制重绑 |
是 |
设备已 active 且 key_version/pub_key 匹配 |
否 |
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
device_id |
string | 是 | UUID 字符串 |
key_version |
integer | 是 | 正整数,首次一般为 1 |
pub_key |
string | 首绑/重绑必填 | P-256 未压缩点 base64url |
force_rebind |
boolean | 否 | 强制重绑新公钥 |
station_id |
string | 视场景 | 工位 ID |
timestamp |
integer | 视场景 | Unix 秒 |
nonce |
string | 视场景 | 防重放随机串 |
sig_b64 |
string | 视场景 | 工位签名 |
challenge 工位签名 payload 只包含非空字段:
{
"device_id": "...",
"key_version": 1,
"pub_key": "...",
"force_rebind": false,
"station_id": "station-001",
"timestamp": 1778192400,
"nonce": "..."
}
成功响应:
{
"challenge_id": "base64url-16bytes",
"challenge": "base64url-32bytes",
"expires_in": 60,
"server_time": 1778192400
}
8.8 提交挑战响应并获取设备 JWT
POST /register/api/auth/response
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
device_id |
string | 是 | UUID |
challenge_id |
string | 是 | challenge 接口返回 |
signature |
string | 是 | 设备私钥签名,base64url(raw r |
station_id |
string | pending 阶段必填 | 工位 ID |
timestamp |
integer | pending 阶段必填 | Unix 秒 |
nonce |
string | pending 阶段必填 | 防重放随机串 |
sig_b64 |
string | pending 阶段必填 | 工位签名 |
成功响应:
{
"access_token": "<device-jwt>",
"token_ttl": 600
}
如果使用上一把仍在宽限期内的 key,响应可能包含:
{
"access_token": "<device-jwt>",
"token_ttl": 600,
"rotate_required": true,
"server_key_version": 2
}
8.9 设备密钥轮换
POST /register/api/auth/rotate
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
device_id |
string | 是 | UUID |
key_version |
integer | 是 | 当前 key version |
new_pub_key |
string | 是 | 新 P-256 公钥 base64url |
new_pub_key_jkt |
string | 是 | 新公钥指纹 |
signature |
string | 是 | 当前私钥对 rotate 消息签名,base64url(raw r |
成功响应:
{
"key_version": 2
}
9. Token 服务所有接口
Token 接口都需要设备 JWT,且必须包含 voice:session scope。
通用 Header:
Authorization: Bearer <device_access_token>
Content-Type: application/json
9.1 获取阿里云临时 Token
POST /token/api/device/token
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
deviceId |
string | 与 serialNumber 至少一个 |
设备 ID |
serialNumber |
string | 与 deviceId 至少一个 |
设备序列号 |
appId |
string | 否 | DTO 中存在,当前设备接口实际使用数据库 device_app 配置 |
workspaceId |
string | 否 | DTO 中存在,当前设备接口实际使用数据库 device_app 配置 |
voiceId |
string | 否 | DTO 中存在,当前设备接口实际使用数据库 device_app 配置 |
如果同时传 deviceId 和 serialNumber,服务端会校验两者是否匹配。
成功响应:
{
"code": 200,
"message": "获取token成功",
"data": {
"appId": "mm_xxx",
"workspaceId": "llm_xxx",
"voiceId": "kenny",
"token": "dashscope-temp-token",
"deviceId": "550e8400-e29b-41d4-a716-446655440000",
"serialNumber": "0508-0001",
"expires_at": 1778194200,
"tokenExpireTime": "2026-05-08T09:30:00",
"roleKey": "default",
"roleName": "默认角色",
"favoriteTeamCode": "",
"favoriteTeamName": "",
"favoriteTeamStatus": ""
}
}
说明:
| 字段 | 说明 |
|---|---|
token |
阿里云 DashScope 临时 token,默认申请 1800 秒 |
expires_at |
阿里云真实过期秒级时间戳 |
tokenExpireTime |
本地时区格式化时间 |
appId/workspaceId/voiceId |
设备连接语音服务时需要使用 |
roleKey/roleName |
当前应用角色信息,可能为空字符串 |
favoriteTeam* |
收藏球队信息,未配置时为空字符串 |
Token 服务会优先返回 Redis 缓存中的有效 token;距离过期不足 10 秒时刷新。
9.2 强制刷新阿里云临时 Token
POST /token/api/device/token/refresh
鉴权、请求体、响应字段与 /token 相同。区别是忽略现有缓存,强制调用阿里云刷新。
成功响应:
{
"code": 200,
"message": "刷新token成功",
"data": {
"appId": "mm_xxx",
"workspaceId": "llm_xxx",
"voiceId": "kenny",
"token": "new-dashscope-temp-token",
"deviceId": "550e8400-e29b-41d4-a716-446655440000",
"serialNumber": "0508-0001",
"expires_at": 1778194200,
"tokenExpireTime": "2026-05-08T09:30:00",
"roleKey": "",
"roleName": "",
"favoriteTeamCode": "",
"favoriteTeamName": "",
"favoriteTeamStatus": ""
}
}
10. OTA 服务所有接口
10.1 设备上报固件版本并检查升级
POST /ota/api/ota/versionInfoReport
鉴权:
| 层级 | 要求 |
|---|---|
| Gateway | 设备 JWT,有 ota:report scope |
| OTA 服务 | 设备 JWT 二次校验,有 ota:report scope |
Header:
Authorization: Bearer <device_access_token>
Content-Type: application/json
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
deviceId |
string | 是 | 设备 ID |
serialNumber |
string | 否 | 设备序列号,当前代码未参与主要逻辑 |
version |
string | 是 | 当前固件版本,例如 1.0.0 或 v1.0.0 |
isUpdate |
integer | 否 | 设备是否确认升级,版本上报接口当前主要使用 deviceId/version |
无升级响应:
HTTP/1.1 204 No Content
Cache-Control: no-store
需要升级响应:
{
"code": 200,
"message": "固件版本信息上报成功",
"data": {
"version": "1.0.1",
"url": "https://download-url-or-short-link",
"sha256": "64位hex",
"sizeBytes": 1234567,
"force": 0,
"update": 0,
"notUptodate": 1
}
}
字段说明:
| 字段 | 说明 |
|---|---|
version |
最新固件版本 |
url |
下载地址;如果启用 COS 预签名,会返回短期可用预签名 URL;如果启用短链,可能返回 /ota/s/{token} |
sha256 |
固件 SHA-256 hex,设备下载后必须校验 |
sizeBytes |
固件大小,设备可用于下载完整性校验 |
force |
1 强制升级,0 非强制 |
update |
设备是否已确认升级状态 |
notUptodate |
1 需要升级,0 不需要升级 |
异常响应:
| 场景 | HTTP | 响应 |
|---|---|---|
deviceId 为空 |
400 | {"code":500,"message":"设备ID不能为空"} |
version 为空 |
400 | {"code":500,"message":"固件版本不能为空"} |
| OTA 下载链接生成失败且 strict-mode=true | 503 | {"code":503,"message":"OTA下载链接生成失败","data":{"errorCode":"OTA_DOWNLOAD_URL_UNAVAILABLE","reason":"..."}} |
| JWT 缺失/过期/scope 不匹配 | 401 | Gateway 或服务过滤器返回未授权 |
10.2 小程序查询设备版本比对结果
GET /ota/api/program/getVersionInfoCompare/{deviceId}
该接口属于小程序/管理侧设备查询接口,不使用设备 JWT scope。Gateway 按 Program Token 校验:
| 要求 | 说明 |
|---|---|
| Header | Authorization: Bearer <program-device-jwt> |
| 签名 | HS256,密钥为 Gateway jwt.secret |
Claim type |
必须为 device |
Claim sub |
必须等于路径中的 {deviceId} |
成功响应:
{
"code": 200,
"message": "success",
"data": {
"notUptodate": 1,
"version": "1.0.1"
}
}
10.3 小程序上报升级确认
POST /ota/api/program/versionUpdateConfirm
鉴权同上一节 Program Token。Gateway 会从 JSON body 里读取 deviceId,并要求 JWT sub 与其一致。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
deviceId |
string | 是 | 设备 ID |
isUpdate |
integer | 是 | 是否确认升级,通常 1 是、0 否 |
version |
string | 否 | DTO 中存在,当前确认接口不使用 |
serialNumber |
string | 否 | DTO 中存在,当前确认接口不使用 |
成功响应:
{
"code": 200,
"message": "固件版本升级确认成功"
}
10.4 OTA 短链跳转
GET /ota/s/{token}
鉴权:无。Gateway 设备 JWT 忽略 /ota/s/**。
用途:当 OTA 开启短链时,versionInfoReport 返回短链接,设备访问短链接会跳转到真实 COS 预签名 URL。
响应:
| 场景 | HTTP | 说明 |
|---|---|---|
| token 格式非法 | 404 | 无响应体 |
| token 不存在或已过期 | 410 | 无响应体 |
| token 有效 | 307 | Location 为真实下载 URL |
token 格式要求:
^[A-Za-z0-9_-]{8,128}$
11. curl 调用示例
11.1 创建设备
curl -X POST "http://<gateway-host>:8090/register/api/v1/devices" \
-H "Content-Type: application/json" \
-H "X-Api-Key: <api-key>" \
-H "X-Factory-Timestamp: <timestamp>" \
-H "X-Factory-Nonce: <nonce>" \
-H "X-Factory-Signature: <signature>" \
-d '{"deviceId":"550e8400-e29b-41d4-a716-446655440000","deviceName":"0508-0001","userId":1,"appId":"mm_xxx","workspaceId":"llm_xxx","voiceId":"kenny","product":"TQ_cat","version":"1.0.0"}'
11.2 设备获取 challenge
curl -X POST "http://<gateway-host>:8090/register/api/auth/challenge" \
-H "Content-Type: application/json" \
-d '{"device_id":"550e8400-e29b-41d4-a716-446655440000","key_version":1,"pub_key":"<pub_key>","station_id":"station-001","timestamp":1778192400,"nonce":"<nonce>","sig_b64":"<station_sig>"}'
11.3 设备提交 challenge 签名
curl -X POST "http://<gateway-host>:8090/register/api/auth/response" \
-H "Content-Type: application/json" \
-d '{"device_id":"550e8400-e29b-41d4-a716-446655440000","challenge_id":"<challenge_id>","signature":"<device_raw_rs_sig_b64url>"}'
11.4 获取阿里云临时 Token
curl -X POST "http://<gateway-host>:8090/token/api/device/token" \
-H "Authorization: Bearer <device_access_token>" \
-H "Content-Type: application/json" \
-d '{"deviceId":"550e8400-e29b-41d4-a716-446655440000"}'
11.5 OTA 版本上报
curl -X POST "http://<gateway-host>:8090/ota/api/ota/versionInfoReport" \
-H "Authorization: Bearer <device_access_token>" \
-H "Content-Type: application/json" \
-d '{"deviceId":"550e8400-e29b-41d4-a716-446655440000","version":"1.0.0"}'
12. 常见问题排查
| 现象 | 优先检查项 |
|---|---|
/register/api/v1/** 返回 401 |
X-Factory-* 是否缺失、timestamp 是否超过 300 秒、body hash 是否按真实字节计算、空 body hash 是否误用了 SHA256(empty) |
/register/api/v1/** 返回 403 |
X-Api-Key 是否在 Redis gateway:factory:api-key:enabled:{apiKey} 中启用 |
Register 返回 API_KEY_INVALID_OR_DISABLED |
数据库 factory_register_quota 是否启用、是否过期 |
Register 返回 API_KEY_QUOTA_EXCEEDED |
工厂 API Key 注册额度不足 |
| 工位签名失败 | canonical JSON 是否排序且紧凑、是否移除了 sig_b64、ECDSA 是否用了 DER 签名、station_id 是否存在于 secrets 文件 |
challenge 返回 AUTH_PUB_KEY_MISMATCH |
已绑定设备上报了不同 pub_key,需要确认是否应 force_rebind=true |
response 返回 AUTH_SIG_INVALID |
设备签名消息拼接顺序、UUID 16 字节、大端 key_version、raw `r |
| Token/OTA 返回 401 | 设备 JWT 是否过期、scope 是否包含 voice:session 或 ota:report、Gateway Redis auth 状态是否 active、kv 是否匹配 |
| OTA 一直 204 | 当前版本已不低于最新版本,或产品 product 没有匹配到可比较最新版本 |
| OTA 503 | COS/短链配置不完整,且 OTA_DOWNLOAD_STRICT_MODE=true |
13. 代码依据
| 模块 | 文件 |
|---|---|
| Gateway 工厂 HMAC | gateway/src/main/java/com/talkingq/gateway/filter/FactoryAuthGlobalFilter.java |
| Gateway 设备 JWT | gateway/src/main/java/com/talkingq/gateway/filter/DeviceJwtAuthGlobalFilter.java |
| Gateway Program JWT | gateway/src/main/java/com/talkingq/gateway/filter/JwtAuthGlobalFilter.java |
| Gateway 路由与鉴权配置 | gateway/src/main/resources/application.yml |
| 注册设备接口 | register/src/main/java/com/talkingq/controller/DeviceController.java |
| 工位签名校验 | register/src/main/java/com/talkingq/deviceauth/service/StationSignatureVerifier.java |
| 工厂接口 | register/src/main/java/com/talkingq/deviceauth/controller/FactoryAuthController.java |
| 设备认证接口 | register/src/main/java/com/talkingq/deviceauth/controller/DeviceAuthController.java |
| 设备认证实现 | register/src/main/java/com/talkingq/deviceauth/service/impl/DeviceAuthServiceImpl.java |
| 设备 JWT 签发 | register/src/main/java/com/talkingq/deviceauth/jwt/DeviceJwtSigner.java |
| Token 接口 | token/src/main/java/com/talkingq/controller/DeviceTokenController.java |
| Token 实现 | token/src/main/java/com/talkingq/service/impl/TokenServiceImpl.java |
| 阿里云 token 调用 | token/src/main/java/com/talkingq/dialogue/token/providers/AliyunAppTokenService.java |
| OTA 设备接口 | ota/src/main/java/com/talkingq/controller/OTAController.java |
| OTA 小程序接口 | ota/src/main/java/com/talkingq/controller/OTAProgramController.java |
| OTA 短链接口 | ota/src/main/java/com/talkingq/controller/OtaShortLinkController.java |
| OTA 版本逻辑 | ota/src/main/java/com/talkingq/service/impl/DeviceUpdateServiceImpl.java |
| OTA 下载 URL | ota/src/main/java/com/talkingq/service/impl/OtaDownloadUrlService.java |