Skip to content

请求签名

推荐直接使用示例客户端:传入 appID、密钥、accessToken 和业务参数即可,客户端完成查询编码、一次序列化、签名及发送。无需新增证书、密钥或请求 nonce。

当前唯一请求协议是 HMAC-SHA256。此前的 MD5 方案已移除,不能继续发送旧签名。回调仍使用原来的 AES-256-GCM 加密协议。

四个请求头

请求头内容
X-App-IdappID 原始十进制字符串
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 完全一致,使用秒,不是毫秒
accessTokenBearer 后面的令牌原文,不包含 Bearer 和空格;获取令牌时本行为空
canonicalQuery按下文生成,不带问号;无查询参数时本行为空
body 摘要实际发送字节的 SHA-256,小写十六进制;不是整个 JSON 对象的字符串相加

appSecret 是原始 UTF-8 字符串。即使看起来像十六进制,也不要先做 hex 解码。HTTP 方法、路径、所有查询和请求体字段(包括 request_id)都受签名保护。

查询参数编码

  1. 每个键只能出现一次,键和值使用 UTF-8 字符串,键不能为空。
  2. 按键的 UTF-8 字节顺序升序排列。
  3. 键和值分别按 RFC 3986 编码,只保留字母、数字及 -._~。
  4. 用 key=value 连接,再用 & 连接各项。
输入规范查询串
page_size=20&page=1page=1&page_size=20
z=%2b+&a=%e4%b8%ada=%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
创建钱包 JSON7bf042060171f07f2138f375605d902c4fdbcec83b6f8424d9494669d4300644

Go、Node.js 和 Python 必须得到相同结果。修改方法、路径、参数、金额或请求编号后,原签名必须失效。在线返回 INVALID_SIGNATURE 时,按错误排查检查。