Files
lucky-cat/docs/TalkingQ_硬件端注册TokenOTA全流程接口文档.md
T
2026-06-25 00:34:34 +08:00

32 KiB
Raw Blame History

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

服务端校验规则:

  1. 从请求体中移除 sig_b64。
  2. 对剩余 JSON 按 key 排序序列化。
  3. 使用工位公钥验签。
  4. 校验 timestamp 默认 300 秒时间窗。
  5. 将 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/** 会校验:

  1. JWT ES256 签名正确。
  2. exp 未过期。
  3. scope 命中接口规则。
  4. Redis 中 gateway:device:auth:{deviceId} 的 auth_status=active。
  5. JWT kv 等于当前 key_version,或等于仍在宽限期的 prev_key_version。

7. 推荐硬件调用顺序

7.1 工厂生产阶段

  1. 工厂工具生成设备 UUID deviceId 和序列号 serialNumber。
  2. 调用 POST /register/api/v1/devices 或 /batch 创建设备和默认应用配置。
  3. 设备生成 P-256 密钥对。
  4. 工厂工具或设备调用 POST /register/api/auth/challenge,首绑时带 station_id/timestamp/nonce/sig_b64/pub_key/key_version。
  5. 设备签名 challenge 后调用 POST /register/api/auth/response 获取设备 JWT。
  6. 设备保存私钥、device_id、key_version。不要保存工厂 HMAC secret 或工位私钥到设备固件。

7.2 设备运行阶段

  1. 若没有有效设备 JWT,重新走 challenge/response。
  2. 携带设备 JWT 调用 POST /token/api/device/token 获取阿里云临时 token。
  3. 使用返回的 token/appId/workspaceId/voiceId 接入语音链路。
  4. 定期或开机调用 POST /ota/api/ota/versionInfoReport 上报固件版本。
  5. 收到 204 表示无需升级;收到 200 且 data.notUptodate=1 表示需要升级。
  6. 下载固件,校验 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