# 签名与鉴权 HMAC-SHA256 签名的拼接规则,各接口的被签名参数与跨端测试向量。 来源:https://open.shengzhiai.com/sdk/v2/api/authentication/ 两种方式任选其一,声通兼容整段评测 `POST /{coreType}` 只接受签名,其余接口两种方式通用。 ## Bearer Token 请求头 `Authorization: Bearer `,实时评测握手在 query 里带 `token=`。token 由登录接口签发,适合控制台与已登录用户。 ## 签名 面向持有 appKey 与 secretKey 的接入方服务端,各端 SDK 联调阶段也可以使用。 | 请求头 | 含义 | |---|---| | `X-App-Key` | appKey | | `X-Timestamp` | 当前 Unix 时间戳,单位为秒 | | `X-Nonce` | 随机串,服务端 300 秒内去重防重放,建议每次都带 | | `X-Signature` | 签名值 | 签名算法: 1. 取本次请求的业务参数,不含请求头,不含文件二进制。 2. 丢弃值为 null 或空字符串的项。 3. 按参数名的字典序升序排列。 4. 拼成 `k1=v1&k2=v2`,不做 URL 编码,末尾不追加 secret。 5. `X-Signature = Base64(HMAC_SHA256(payload, secretKey))`,payload 与 secretKey 都按 UTF-8 编码。 6. 服务端校验 `|当前秒 - X-Timestamp| ≤ 300`,超出即拒绝。 ## 各接口的被签名参数 | 接口 | 被签名参数 | |---|---| | 原生整段评测 `POST /api/v1/evaluate` | `config` 段不带文件名时,该段是一个表单参数,被签名参数为 `{config: config 段的 JSON 原文}` 加其余文本段。SDK 2.0 一律用这种方式。 | | 声通兼容整段评测 `POST /{coreType}` | 实际发送的全部文本表单字段,音频不参与 | | 语音合成 `POST /api/v1/tts/generate` | 请求体 JSON 的顶层非空标量字段,值取 JSON 里的字面文本,数值只写普通十进制,不写指数形式 | | 报告查询 `GET /api/v1/report/{recordId}` | 空集合,recordId 是路径的一段,不参与 | | 实时评测握手 | 除 `signature` 外的全部 query 参数,含 appKey,timestamp,nonce,idempotencyKey | 服务端对原生整段评测同时接受另一种口径:表单参数去掉 `config` 后,加上 config JSON 的顶层非空标量字段。浏览器 `FormData` 发送的 `config` 段带文件名 `blob`,只能按这种口径签名。 跨端一致性测试向量在 `spec/fixtures/sign/vectors.json`,其中契约向量为: ``` secret = test_secret_key_123 params = {coreType: sent.eval.cn, language: zh-CN, refText: 北京你好} payload = coreType=sent.eval.cn&language=zh-CN&refText=北京你好 expected = A+6uVB/D7khxQEt8tzgCNjMUC1QtQQd1UF+NCYVYZqE= ``` ## 签名示例 各语言按同一规则计算,结果与契约向量一致即可。 Shell: ```bash # 签名:业务参数去掉空值,按键名字典序拼成 k1=v1&k2=v2,不做 URL 编码 PAYLOAD='coreType=sent.eval.cn&language=zh-CN&refText=北京你好' printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac 'test_secret_key_123' -binary | base64 # 输出 A+6uVB/D7khxQEt8tzgCNjMUC1QtQQd1UF+NCYVYZqE= ``` Python: ```python import base64, hashlib, hmac def sign(params: dict, secret_key: str) -> str: # 丢弃 None 与空串,按键名字典序拼成 k1=v1&k2=v2,不做 URL 编码 payload = '&'.join(f'{k}={v}' for k, v in sorted(params.items()) if v not in (None, '')) digest = hmac.new(secret_key.encode('utf-8'), payload.encode('utf-8'), hashlib.sha256).digest() return base64.b64encode(digest).decode('ascii') print(sign({'coreType': 'sent.eval.cn', 'language': 'zh-CN', 'refText': '北京你好'}, 'test_secret_key_123')) # 输出 A+6uVB/D7khxQEt8tzgCNjMUC1QtQQd1UF+NCYVYZqE= ``` Node.js: ```js import crypto from 'node:crypto'; function sign(params, secretKey) { // 丢弃 null 与空串,按键名字典序拼成 k1=v1&k2=v2,不做 URL 编码 const payload = Object.keys(params).sort() .filter((k) => params[k] !== null && params[k] !== undefined && params[k] !== '') .map((k) => `${k}=${params[k]}`).join('&'); return crypto.createHmac('sha256', secretKey).update(payload, 'utf8').digest('base64'); } console.log(sign({ coreType: 'sent.eval.cn', language: 'zh-CN', refText: '北京你好' }, 'test_secret_key_123')); // 输出 A+6uVB/D7khxQEt8tzgCNjMUC1QtQQd1UF+NCYVYZqE= ``` Java: ```java import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public class Sign { static String sign(Map params, String secretKey) throws Exception { // 丢弃 null 与空串,按键名字典序拼成 k1=v1&k2=v2,不做 URL 编码 String payload = new TreeMap<>(params).entrySet().stream() .filter(e -> e.getValue() != null && !e.getValue().isEmpty()) .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining("&")); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); return Base64.getEncoder().encodeToString(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8))); } public static void main(String[] args) throws Exception { System.out.println(sign(Map.of("coreType", "sent.eval.cn", "language", "zh-CN", "refText", "北京你好"), "test_secret_key_123")); // 输出 A+6uVB/D7khxQEt8tzgCNjMUC1QtQQd1UF+NCYVYZqE= } } ``` ## 测试向量 `spec/fixtures/sign/vectors.json` 收录 7 组向量,覆盖契约向量,原生 config 段,兼容表单,语音合成请求体,实时评测握手,空集合与特殊字符,五端 SDK 的签名单测逐条命中。 ```json { "name": "contract_vector", "secret": "test_secret_key_123", "params": { "coreType": "sent.eval.cn", "language": "zh-CN", "refText": "北京你好" }, "payload": "coreType=sent.eval.cn&language=zh-CN&refText=北京你好", "signature": "A+6uVB/D7khxQEt8tzgCNjMUC1QtQQd1UF+NCYVYZqE=" } ```