AUTHENTICATION
HMAC-SHA256 鉴权
派付的所有写接口都强制带 4 个 HMAC header,缺一不可。本章把签名算法、防重放策略、踩坑点全部讲清楚——读完应该再也不需要回头查。
关于 apiSecret 的安全保管
apiSecret 仅在创建应用时返回一次。请立即放进你自己的密钥管理系统(KMS / Vault / k8s Secret),不要提交到代码仓库、不要写在前端、不要打印到日志。 一旦泄漏,立即去后台「应用列表 → 重置密钥」轮换。 1. 四个必传 header
每一个鉴权请求都必须带齐这 4 个 header:
X-Api-Keystring required | 应用的 apiKey,创建应用时由后台返回。沙箱环境前缀 pk_test_,正式环境前缀 pk_live_。 例: pk_test_4f9d8ab1c0e74... |
X-Timestampinteger (UNIX 毫秒) required | 请求发起时刻的 UNIX 毫秒时间戳(13 位整数)。与服务端时差不能超过 ±300000ms(5 分钟),否则返回 SIGN_TIMESTAMP_EXPIRED (1_007_002_002)。 例: 1746450000000 |
X-Noncestring required | 本次请求的随机串,建议 UUID v4 或 16 字节 hex。同一 (X-Api-Key, X-Nonce) 在 10 分钟窗口内只允许出现一次,重复返回 SIGN_NONCE_DUPLICATE (1_007_002_003)。 例: 8e3a1c2f7b6d4a90 |
X-Signaturestring required | HMAC-SHA256 签名结果,小写 hex 编码。计算见下方算法。 例: 6e4a8c1f...(64 个字符) |
2. 签名算法
stringToSign 由 5 段拼成,分隔符是换行符 \n(字面 LF,不是字符串 "\n"):
stringToSign = METHOD + "\n" + PATH + "\n" + X-Timestamp + "\n" + X-Nonce + "\n" + SHA256_hex(rawBody)
signature = HMAC_SHA256(apiSecret, stringToSign).hex().toLowerCase()各段说明
METHOD:HTTP 方法大写,如GET / POST / PUT / DELETE。PATH:URL 的 path 部分,不含 query string、不含https://host:port,如/api/v1/wallet/address/create。query 参数不参与签名。X-Timestamp/X-Nonce:直接用 header 里的值(timestamp 是 UNIX 毫秒)。SHA256_hex(rawBody):把实际发送的 HTTP body 的 UTF-8 字节原文做 SHA-256,输出 64 字符小写 hex。注意进签名串的是 body 的哈希,不是 body 原文。- GET 请求没有 body?那 rawBody 就是空字符串
"",它的 SHA-256 是固定值e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。 - body 只 JSON.stringify 一次就好,千万不要序列化两次——服务端按你发送的字节重算 SHA-256,多一个空格都会失败。
最常见的踩坑
- 用秒级 timestamp(必须毫秒级,13 位)。
- 把 body 原文直接拼进签名串(必须先对 body 做 SHA-256 再拼)。
- 漏掉
METHOD/PATH,或把 query string 拼进了PATH。 - 分隔符用了句号
.而不是换行符\n。 - 用序列化后的 JSON 对象而不是字符串去算哈希(导致 key 顺序 / 空白与发送时不一致)。
- HMAC 输出用了大写 hex(必须小写)。
3. 多语言示例
下面几段代码做的事完全一致:拿 secret、method、path 和 body,吐出 (ts, nonce, sig)。
import crypto from 'node:crypto';
/**
* stringToSign = METHOD + "\n" + PATH + "\n" + ts + "\n" + nonce + "\n" + SHA256_hex(rawBody)
* - METHOD 大写;PATH 是 URL path(不含 query、不含 host),如 /api/v1/wallet/address/create
* - ts 是 UNIX 毫秒(13 位)
* - rawBody 先做 SHA-256(小写 hex)再进签名串;GET 空 body 也要算(结果是固定值 e3b0c4...)
*/
function sign(secret, method, path, body = '') {
const ts = Date.now().toString(); // UNIX 毫秒
const nonce = crypto.randomUUID().replace(/-/g, '');
const bodyHash = crypto.createHash('sha256').update(body, 'utf8').digest('hex');
const stringToSign = [method.toUpperCase(), path, ts, nonce, bodyHash].join('\n');
const sig = crypto
.createHmac('sha256', secret)
.update(stringToSign, 'utf8')
.digest('hex'); // 小写 hex
return { ts, nonce, sig };
}
// 用法
const path = '/api/v1/wallet/address/create';
const body = JSON.stringify({ chainCode: 'ETH', externalUserId: 'user_1024' });
const { ts, nonce, sig } = sign(process.env.PQPA_SECRET, 'POST', path, body);
await fetch('https://api.pqpa.com' + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': process.env.PQPA_API_KEY,
'X-Timestamp': ts,
'X-Nonce': nonce,
'X-Signature': sig,
},
body,
});4. 防重放策略
派付服务端会针对每个鉴权请求做三层检查:
- 时间窗口:
|server_now - X-Timestamp| ≤ 300000ms(5 分钟)。超出直接返回SIGN_TIMESTAMP_EXPIRED (1_007_002_002)。这要求你的服务器时钟通过 NTP 校准——NTP 偏移 5 分钟以上接派付会全部失败。 - Nonce 去重:同一
(X-Api-Key, X-Nonce)在 10 分钟窗口内只允许出现一次,重复返回SIGN_NONCE_DUPLICATE (1_007_002_003)。 所以 nonce 不要用单调递增整数(万一你的进程重启 nonce 计数器从 0 开始就会报错),用 UUID v4 是最稳的。 - 签名校验:以上两步都过之后才做 HMAC 比较;任何一步失败都返回 401。
5. 排错
99% 的"我签名怎么都对不上"问题是 rawBody 不一致。 建议在客户端集成一个自检步骤——在发送前用同一份 secret + body 自己算一遍签名, 和将要发送出去的签名比对:
// 把这段加到你接入的代码里,签完拍上去之前先在本机校验一遍
import crypto from 'node:crypto';
function verify(secret, method, path, ts, nonce, body, sig) {
const bodyHash = crypto.createHash('sha256').update(body, 'utf8').digest('hex');
const stringToSign = [method.toUpperCase(), path, ts, nonce, bodyHash].join('\n');
const expected = crypto
.createHmac('sha256', secret)
.update(stringToSign, 'utf8')
.digest('hex');
if (expected !== sig) {
console.error('[PQPA] 签名不一致!');
console.error(' expected:', expected);
console.error(' actual: ', sig);
console.error(' stringToSign:', JSON.stringify(stringToSign));
return false;
}
return true;
}还是搞不定?
把请求的所有 header + HTTP body 的 hex dump (前 200 字节够了)+ X-Trace-Id 发给我们,我们能在服务端日志里反查到拒绝原因。 提交工单 时也带上。6. 密钥轮换
派付支持双密钥并行过渡期,让你能无停机轮换:
- 后台「应用列表 → 重置密钥」时,会同时返回
newSecret+oldSecret。 - 派付服务端在接下来 24 小时同时接受这两个 secret 签名出来的请求。
- 你在这 24 小时窗口内把所有上游服务的 secret 替换成
newSecret。 - 24 小时后
oldSecret自动失效。
下一步
🎯 学会签名后,建议直接去 创建充值地址 跑一遍真实请求——这是验证签名是否正确的最快路径。