Skip to content

HMAC 鉴权

Coinepay OpenAPI 使用 HMAC-SHA256 对每次请求签名。客户必须在每次请求头中提供 4 个值;服务器据此校验身份 + 时间窗 + 防重放 + 防篡改

必需的 4 个请求头

Header说明示例
X-App-Id凭证标识,固定 31 字符(cp_ + 28 hex)cp_a1b2c3d4e5f6071829304a5b6c7d8e9f
X-TimestampUnix (不是毫秒),ASCII 十进制1714377600
X-Nonce8~64 位字符,10 分钟内对同一 AppID 唯一8f7e6d5c4b3a29180a1b2c3d4e5f6071
X-SignatureHMAC-SHA256 lowercase hex,64 字符9b8e7f6d5c4b3a...

时间单位

X-Timestamp 必须是,不是毫秒。Math.floor(Date.now() / 1000) 不是 Date.now()

签名输入构造

text
signInput = METHOD + LF + PATH + LF + RAW_QUERY + LF + TIMESTAMP + LF + NONCE + LF + BODY_SHA256_HEX
符号含义
LF字符 \n(单字节 0x0A),不是 \r\n
+字符串拼接

各字段定义

字段取值注意
METHOD全大写 HTTP 方法(OpenAPI 全部为 POSTASCII
PATH请求路径含开头 /不含查询串不要 URL decode/encode
RAW_QUERY查询串(不含 ?),无则空字符串通常为空
TIMESTAMPX-Timestamp完全相同字符串ASCII
NONCEX-Nonce 头完全相同ASCII
BODY_SHA256_HEXsha256(请求体字节) 的 lowercase hex64 字符

空 body 的 sha256

固定为 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。 空对象 {} 的 sha256 是 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a(注意区别)。

签名计算

text
signature = lowercase_hex( HMAC_SHA256( secret_bytes, signInput_bytes ) )

Secret 编码

secret_bytes 是 secret 字符串的 UTF-8 字节(直接是 64 个 hex 字符的 ASCII 字节)。 不要把 hex decode 成 32 字节再做 HMAC。

签名长度固定为 64 字符 hex

完整请求示例

http
POST /api/v1/openapi/card_headers/list HTTP/1.1
Host: api.coinepay.net
X-App-Id: cp_a1b2c3d4e5f6071829304a5b6c7d8e9f
X-Timestamp: 1714377600
X-Nonce: 8f7e6d5c4b3a29180a1b2c3d4e5f6071
X-Signature: 9b8e7f6d5c4b3a2918273645d4e3c2b1a0f9e8d7c6b5a4938271605f4e3d2c1b
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
Content-Length: 24

{"page":1,"page_size":20}

防重放窗口

时间戳容差[server_now - 300s, server_now + 300s] (±5 分钟)
Nonce 唯一窗口同一 (app_id, nonce)10 分钟内不允许重复
推荐 Nonce 生成crypto/rand 16 字节 → 32 hex 字符

极端情况速查

场景处理
body 是空对象 {}BODY_SHA256_HEX = 44136fa3...8a
body 是空字符串用空 body sha256 常量 e3b0c44...855
body 含中文UTF-8 字节计算 sha256
body 是数组 [1,2,3]按字节字面计算(key 排序、空格不影响哈希)
网络中间件改 body签名失败;避免任何中间层改 body

实现注意事项

客户端 stringify 必须确定性

不同语言的 JSON 序列化可能有键序、空格、转义差异。客户端 stringify 出来的字节是什么,就用什么计算 sha256,传给服务器的也是同一份字节。不要先 stringify 计算 hash、又用另一种序列化发送。

401 错误处理

服务器对所有鉴权失败原因(AppID 不存在 / 签名错 / 时间漂移 / 凭证禁用 / 用户不活跃)都返回相同的 openapi_invalid_credentials。这是反枚举设计,客户端无需根据具体原因区分。

排查步骤:

  1. 确认服务器时间与客户端时间差 < 5 分钟(用 date +%s 对一下)
  2. 打印 signInput 的字节,逐行确认 \n0x0A、没有 \r
  3. 确认 X-Timestamp 头与 signInput 中的 TIMESTAMP 完全一致(不是各算一次)
  4. 确认 BODY_SHA256_HEX 的 body 是实际发送的字节(不是 stringify 又改过的)
  5. 重置一对凭证再试

下一步

采用 MIT 等价条款发布