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

1045 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TalkingQ 硬件端注册、临时 Token、OTA 全流程接口文档
版本:2026-05-08
适用对象:硬件固件、产测工具、工厂烧录工具开发同事
代码依据:`gateway`、`register`、`token`、`ota` 当前源码
## 1. 总览
默认统一从 Gateway 调用:
```text
http://<gateway-host>:8090
```
不建议设备或工厂工具直连下游服务端口。Gateway 会统一做外部鉴权、限流、单飞控制,并自动给下游服务补充内部调用签名 `X-Internal-*`。
下游服务端口仅用于定位问题:
| 服务 | 默认端口 | Gateway 路由前缀 |
|---|---:|---|
| gateway | 8090 | - |
| register | 8097 | `/register/**` |
| token | 8095 | `/token/**` |
| ota | 8096 | `/ota/**` |
## 2. 全流程逻辑示意图
```mermaid
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]
```
鉴权分层:
```mermaid
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 工厂鉴权只拦截:
```text
/register/api/v1/**
```
也就是以下设备注册接口必须带 `X-Factory-*`:
```text
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`:
```text
<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` | 与请求头完全一致 |
签名计算:
```text
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 签名示例
```python
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`
工位签名用于工厂刷写/解锁/上报,以及设备首次绑定或强制重绑时证明“这台设备是在合法工位上生产/绑定”。
涉及接口:
```text
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 文件。支持如下形式:
```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:
```python
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 生成工位签名示例
```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` 格式:
```text
base64url_no_padding( 0x04 || X[32] || Y[32] )
```
其中 `X`、`Y` 是 P-256 公钥坐标,固定 32 字节大端。
### 6.2 公钥指纹 `pub_key_jkt`
服务端按 JWK thumbprint 思路计算:
```json
{"crv":"P-256","kty":"EC","x":"<base64url(X)>","y":"<base64url(Y)>"}
```
然后:
```text
pub_key_jkt = base64url_no_padding( SHA256( compact_sorted_jwk_json_utf8 ) )
```
### 6.3 challenge 签名消息
设备调用 `/register/api/auth/challenge` 后,服务返回 `challenge_id` 和 `challenge`。设备用设备私钥签名下面的原始字节:
```text
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` 格式:
```text
base64url_no_padding( raw_r_32 || raw_s_32 )
```
注意这里和工位签名不同:设备签名要求 raw `r||s`,不是 DER。
### 6.4 rotate 签名消息
密钥轮换时,设备用当前私钥签名:
```text
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)
```
提交格式仍是:
```text
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 创建设备
```text
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` |
成功响应:
```json
{
"code": 200,
"message": "设备新建成功,app配置添加成功。",
"data": {
"deviceId": "550e8400-e29b-41d4-a716-446655440000",
"deviceName": "0508-0001",
"product": "TQ_cat",
"version": "1.0.0"
}
}
```
### 8.2 查询设备
```text
GET /register/api/v1/devices/{deviceId}
```
鉴权同创建设备,必须带 `X-Factory-*` 和 `X-Api-Key`。
响应:
```json
{
"code": 200,
"message": "success",
"data": {
"deviceId": "550e8400-e29b-41d4-a716-446655440000",
"deviceName": "0508-0001",
"product": "TQ_cat",
"version": "1.0.0"
}
}
```
### 8.3 批量创建设备
```text
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"` |
成功响应:
```json
{
"code": 200,
"message": "操作成功",
"data": {
"createdCount": 2,
"appConfigCount": 2
}
}
```
### 8.4 工厂申请刷写密钥
```text
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 工厂申请解锁凭据
```text
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 | 是 | 工位签名 |
成功响应包含:
```json
{
"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 工厂上报刷机/注册结果
```text
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 | 是 | 工位签名 |
成功响应:
```json
{
"status": "ok",
"report_id": "uuid",
"server_time": 1778192400
}
```
### 8.7 发起设备认证挑战
```text
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 只包含非空字段:
```json
{
"device_id": "...",
"key_version": 1,
"pub_key": "...",
"force_rebind": false,
"station_id": "station-001",
"timestamp": 1778192400,
"nonce": "..."
}
```
成功响应:
```json
{
"challenge_id": "base64url-16bytes",
"challenge": "base64url-32bytes",
"expires_in": 60,
"server_time": 1778192400
}
```
### 8.8 提交挑战响应并获取设备 JWT
```text
POST /register/api/auth/response
```
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `device_id` | string | 是 | UUID |
| `challenge_id` | string | 是 | challenge 接口返回 |
| `signature` | string | 是 | 设备私钥签名,base64url(raw r||s) |
| `station_id` | string | pending 阶段必填 | 工位 ID |
| `timestamp` | integer | pending 阶段必填 | Unix 秒 |
| `nonce` | string | pending 阶段必填 | 防重放随机串 |
| `sig_b64` | string | pending 阶段必填 | 工位签名 |
成功响应:
```json
{
"access_token": "<device-jwt>",
"token_ttl": 600
}
```
如果使用上一把仍在宽限期内的 key,响应可能包含:
```json
{
"access_token": "<device-jwt>",
"token_ttl": 600,
"rotate_required": true,
"server_key_version": 2
}
```
### 8.9 设备密钥轮换
```text
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||s) |
成功响应:
```json
{
"key_version": 2
}
```
## 9. Token 服务所有接口
Token 接口都需要设备 JWT,且必须包含 `voice:session` scope。
通用 Header:
```text
Authorization: Bearer <device_access_token>
Content-Type: application/json
```
### 9.1 获取阿里云临时 Token
```text
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`,服务端会校验两者是否匹配。
成功响应:
```json
{
"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
```text
POST /token/api/device/token/refresh
```
鉴权、请求体、响应字段与 `/token` 相同。区别是忽略现有缓存,强制调用阿里云刷新。
成功响应:
```json
{
"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 设备上报固件版本并检查升级
```text
POST /ota/api/ota/versionInfoReport
```
鉴权:
| 层级 | 要求 |
|---|---|
| Gateway | 设备 JWT,有 `ota:report` scope |
| OTA 服务 | 设备 JWT 二次校验,有 `ota:report` scope |
Header:
```text
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` |
无升级响应:
```text
HTTP/1.1 204 No Content
Cache-Control: no-store
```
需要升级响应:
```json
{
"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 小程序查询设备版本比对结果
```text
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}` |
成功响应:
```json
{
"code": 200,
"message": "success",
"data": {
"notUptodate": 1,
"version": "1.0.1"
}
}
```
### 10.3 小程序上报升级确认
```text
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 中存在,当前确认接口不使用 |
成功响应:
```json
{
"code": 200,
"message": "固件版本升级确认成功"
}
```
### 10.4 OTA 短链跳转
```text
GET /ota/s/{token}
```
鉴权:无。Gateway 设备 JWT 忽略 `/ota/s/**`。
用途:当 OTA 开启短链时,`versionInfoReport` 返回短链接,设备访问短链接会跳转到真实 COS 预签名 URL。
响应:
| 场景 | HTTP | 说明 |
|---|---:|---|
| token 格式非法 | 404 | 无响应体 |
| token 不存在或已过期 | 410 | 无响应体 |
| token 有效 | 307 | `Location` 为真实下载 URL |
`token` 格式要求:
```text
^[A-Za-z0-9_-]{8,128}$
```
## 11. curl 调用示例
### 11.1 创建设备
```bash
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
```bash
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 签名
```bash
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
```bash
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 版本上报
```bash
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||s` 编码是否正确 |
| 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` |