HMAC 鑑權
Coinepay OpenAPI 使用 HMAC-SHA256 對每次請求籤名。客戶必須在每次請求頭中提供 4 個值;伺服器據此校驗身份 + 時間窗 + 防重放 + 防篡改。
必需的 4 個請求頭
| Header | 說明 | 示例 |
|---|---|---|
X-App-Id | 憑證標識,固定 31 字元(cp_ + 28 hex) | cp_a1b2c3d4e5f6071829304a5b6c7d8e9f |
X-Timestamp | Unix 秒(不是毫秒),ASCII 十進位制 | 1714377600 |
X-Nonce | 8~64 位字元,10 分鐘內對同一 AppID 唯一 | 8f7e6d5c4b3a29180a1b2c3d4e5f6071 |
X-Signature | HMAC-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 全部為 POST) | ASCII |
PATH | 請求路徑含開頭 /,不含查詢串 | 不要 URL decode/encode |
RAW_QUERY | 查詢串(不含 ?),無則空字串 | 通常為空 |
TIMESTAMP | 與 X-Timestamp 頭完全相同字串 | ASCII |
NONCE | 與 X-Nonce 頭完全相同 | ASCII |
BODY_SHA256_HEX | sha256(請求體位元組) 的 lowercase hex | 64 字元 |
空 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。這是反列舉設計,客戶端無需根據具體原因區分。
排查步驟:
- 確認伺服器時間與客戶端時間差 < 5 分鐘(用
date +%s對一下) - 列印
signInput的位元組,逐行確認\n是0x0A、沒有\r - 確認
X-Timestamp頭與 signInput 中的TIMESTAMP完全一致(不是各算一次) - 確認
BODY_SHA256_HEX的 body 是實際傳送的位元組(不是 stringify 又改過的) - 重置一對憑證再試