两种方式任选其一,声通兼容整段评测 POST /{coreType} 只接受签名,其余接口两种方式通用。
Bearer Token#
请求头 Authorization: Bearer <jwt>,实时评测握手在 query 里带 token=<jwt>。token 由登录接口签发,适合控制台与已登录用户。
签名#
面向持有 appKey 与 secretKey 的接入方服务端,各端 SDK 联调阶段也可以使用。
| 请求头 | 含义 |
|---|---|
X-App-Key |
appKey |
X-Timestamp |
当前 Unix 时间戳,单位为秒 |
X-Nonce |
随机串,服务端 300 秒内去重防重放,建议每次都带 |
X-Signature |
签名值 |
签名算法:
- 取本次请求的业务参数,不含请求头,不含文件二进制。
- 丢弃值为 null 或空字符串的项。
- 按参数名的字典序升序排列。
- 拼成
k1=v1&k2=v2,不做 URL 编码,末尾不追加 secret。 X-Signature = Base64(HMAC_SHA256(payload, secretKey)),payload 与 secretKey 都按 UTF-8 编码。- 服务端校验
|当前秒 - 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
# 签名:业务参数去掉空值,按键名字典序拼成 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
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=JavaScript
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
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<String, String> 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 的签名单测逐条命中。
契约向量
{
"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="
}