请求签名
推荐直接使用示例客户端:传入 appID、密钥、accessToken 和业务参数即可,客户端完成查询编码、一次序列化、签名及发送。无需新增证书、密钥或请求 nonce。
当前唯一请求协议是 HMAC-SHA256。此前的 MD5 方案已移除,不能继续发送旧签名。回调仍使用原来的 AES-256-GCM 加密协议。
四个请求头
| 请求头 | 内容 |
|---|---|
| X-App-Id | appID 原始十进制字符串 |
| X-Timestamp | 当前 Unix 秒级时间戳字符串,与服务器相差不超过 300 秒 |
| X-Sign | 按下文计算的 64 位小写十六进制签名 |
| Authorization | 业务请求填 Bearer 加一个空格和 accessToken;获取令牌时不发送 |
密钥仅在本地签名时使用,不放在 URL、请求体或日志中。公网连接使用平台提供的 HTTPS 域名;当前 HTTP 地址仅用于内网联调。
签名文本
先把请求体序列化成将要发送的 UTF-8 字节,计算小写十六进制 SHA-256 摘要。无请求体时,对长度为零的字节计算摘要。
按固定顺序拼接七行,每行末尾都是一个 LF 换行,包括最后一行:
text
method + "\n"
path + "\n"
appID + "\n"
timestamp + "\n"
accessToken + "\n"
canonicalQuery + "\n"
lowercase_hex(SHA256(rawBodyBytes)) + "\n"然后计算:
text
X-Sign = lowercase_hex(HMAC-SHA256(
key = UTF8(appSecret),
message = UTF8(上述七行文本)
))| 字段 | 精确含义 |
|---|---|
| method | 大写 GET 或 POST |
| path | 完整接口路径,例如 /open/v1/wallets/123;不含域名、查询串、末尾斜杠。不做百分号转义或路径规范化 |
| appID | 与 X-App-Id 完全一致,不带前导零或正号 |
| timestamp | 与 X-Timestamp 完全一致,使用秒,不是毫秒 |
| accessToken | Bearer 后面的令牌原文,不包含 Bearer 和空格;获取令牌时本行为空 |
| canonicalQuery | 按下文生成,不带问号;无查询参数时本行为空 |
| body 摘要 | 实际发送字节的 SHA-256,小写十六进制;不是整个 JSON 对象的字符串相加 |
appSecret 是原始 UTF-8 字符串。即使看起来像十六进制,也不要先做 hex 解码。HTTP 方法、路径、所有查询和请求体字段(包括 request_id)都受签名保护。
查询参数编码
- 每个键只能出现一次,键和值使用 UTF-8 字符串,键不能为空。
- 按键的 UTF-8 字节顺序升序排列。
- 键和值分别按 RFC 3986 编码,只保留字母、数字及 -._~。
- 用 key=value 连接,再用 & 连接各项。
| 输入 | 规范查询串 |
|---|---|
| page_size=20&page=1 | page=1&page_size=20 |
| z=%2b+&a=%e4%b8%ad | a=%E4%B8%AD&z=%2B%20 |
| keyword= | keyword= |
空格编码为 %20,字面加号编码为 %2B。百分号后十六进制字母大写;不要再次编码整个查询串。重复键、缺少等号、非法编码都会返回400。每个接口只接受参数表中的字段。
JSON 只序列化一次
正确顺序是:构造对象 → 序列化一次 → 签名这些字节 → 原样发送这些字节。签名后改变字段顺序、空格、字符转义或末尾换行,都会使签名失效。
业务 POST 使用单个 JSON 对象,不在 URL 中传参数;GET 请求体为空。不要把金额或 ID 转成浮点数。
获取 accessToken
同样使用七行规则。method 为 POST,path 为 /open/v1/auth/token;accessToken、canonicalQuery 两行为空,body 摘要为:
text
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855不发送 Authorization,请求体完全为空。空内容不等于 {}、null 或换行。使用 client.issueToken() / client.issue_token() 即可。
Node.js 核心实现
js
import { createHash, createHmac } from 'node:crypto';
function sign(method, path, appId, secret, timestamp, token, query, bodyBytes) {
const bodyHash = createHash('sha256').update(bodyBytes).digest('hex');
const text = [method, path, appId, timestamp, token, query, bodyHash].join('\n') + '\n';
return createHmac('sha256', Buffer.from(secret, 'utf8'))
.update(text, 'utf8').digest('hex');
}完整客户端包括参数规范化、禁止重定向和错误解析,见下载页。
跨语言自测
下载固定测试向量,其中包含完整输入、规范查询串、签名和可解密回调。演示凭据和时间戳只能用于离线测试。
| 场景 | 预期签名 |
|---|---|
| 获取令牌 | 83b1946bee08f97228d31bbe5b33a38c0c52a052ebaba8a00c0b80468bd66065 |
| UTF-8 查询参数 | eeb0c81d999e3138aceb7b26522db0020d07f2d60f28b79d93a1c52e3cf3a589 |
| 创建钱包 JSON | 7bf042060171f07f2138f375605d902c4fdbcec83b6f8424d9494669d4300644 |
Go、Node.js 和 Python 必须得到相同结果。修改方法、路径、参数、金额或请求编号后,原签名必须失效。在线返回 INVALID_SIGNATURE 时,按错误排查检查。