1045 lines
32 KiB
Markdown
1045 lines
32 KiB
Markdown
# 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` |
|