簽名與鑑權
無論哪個方向的呼叫——你呼叫平台,或平台呼叫你的回呼——都使用同一套 HMAC-SHA256 請求簽名機制。@moose/provider-sdk 的 ProviderClient 會自動幫你簽好每一次對外呼叫;只有在診斷失敗、實作回呼處理程式,或是要簽一個 SDK 沒覆蓋到的呼叫時,才需要完整讀完本頁。
Canonical string
method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + bodymethod—— HTTP 方法,大寫(POST)。path—— 僅 URL 路徑,不含協定、主機或查詢字串(例如/v1/wallet/transaction)。timestamp—— Unix 秒數,以字串形式,必須和X-Timestamp請求頭完全一致。nonce—— 一個隨機的一次性 token,必須和X-Nonce請求頭完全一致。generateNonce()產生 16 個隨機位元組,轉為十六進位制。body—— 實際送出的請求主體位元組。沒有主體的請求則為空字串。
HMAC-SHA256(secret, canonicalString),轉為十六進位制後即為簽名。
請求頭
| 請求頭 | 意義 |
|---|---|
X-Tenant-ID | 發出這次呼叫的租戶。當你簽名一個請求時,這裡是你自己的廠商租戶 ID。當平台簽名一個回呼呼叫給你時,這裡仍然是你的廠商租戶 ID——平台是在向你證明「這通呼叫是給你的」,不是在報自己的身份。 |
X-Timestamp | Unix 秒數,和 canonical string 裡的時間戳一致。 |
X-Nonce | 隨機的一次性值,和 canonical string 裡的 nonce 一致。 |
X-Signature | 十六進位制編碼的 HMAC-SHA256 簽名。 |
@moose/provider-sdk 匯出的 signRequest/generateNonce 會幫你組出這四個值;explainSignature 的計算方式相同,但同時會返回它所雜湊的 canonical string,用於診斷簽名不對——見除錯。
時鐘偏差
如果 X-Timestamp 和平台自己的時鐘相差太多,平台會拒絕這個請求——預設容許誤差為正負 300 秒(5 分鐘)。請保持簽名請求(以及驗證入站回呼)的那台伺服器時鐘透過 NTP 同步;容器或虛擬機時鐘偏移是造成間歇性 401 卻找不到明顯原因的常見元兇。onDebug 的 clockSkewMs(見除錯)可以直接看出這個問題。
Nonce 只能使用一次
每個 X-Nonce 只能使用一次。nonce 本身被納入簽名計算,所以攻擊者即使截獲一個已簽名的請求,也無法單純換上一個新的 nonce 來重放它——這樣簽名就對不上了。重複使用一個 nonce(即使簽名本身正確)也會被當作重放攻擊拒絕。每次簽名請求都要生成新的 nonce——ProviderClient 和 signRequest/generateNonce 已經幫你做到這一點。
金鑰輪替
如果你的對接窗口輪替了你租戶的金鑰,平台會在輪替後的一段寬限期內(預設 7 天)繼續接受用你舊金鑰簽出的簽名——讓你有時間切換簽名程式碼,而不必面對一次瞬間的強制切斷。這個寬限期只適用於平台驗證你的對外簽名。請儘快完成輪替,不要依賴這個寬限期一直開著——它是遷移期間的權宜之計,不是穩態的備援機制。
反過來的方向沒有這種寬限期:verifyPlatformSignature(你用來驗證平台回呼呼叫的函式)永遠只檢查你自己當前唯一的金鑰。如果你要輪替金鑰,必須在兩側同一時刻更新——當你是驗證方時,沒有「兩個金鑰都能用」的過渡窗口。
錯誤面
簽名層的失敗和一般業務邏輯的錯誤,靠回應內容的形狀來區分——完整說明見錯誤碼與重試:
| 失敗情況 | 狀態碼 | 響應體 |
|---|---|---|
| 簽名錯誤、缺失,或時間戳過期 | 401 | 純文字:invalid signature |
X-Nonce 缺失或過長(最多 128 字元) | 400 | JSON:{ "error": "invalid nonce" } |
| 一個已經被使用過的 nonce | 401 | JSON:{ "error": "replayed request" } |
純文字的 401 發生在請求還沒進入任何業務邏輯之前——有效的簽名是每一個簽名端點(無論方向)的前提條件。
對稱性:出站與入站
兩個方向的構造方式完全一致:
- 出站(你 → 平台):
signRequest/ProviderClient根據你即將送出的請求組出 canonical string,並附上對應的請求頭。 - 入站(平台 → 你):
verifyPlatformSignature根據你收到的請求重新組出同一個 canonical string,再拿去和X-Signature比對。
入站這一側唯一要小心的地方:body 必須是平台簽名時所用的逐位元組原始內容。如果你的網頁框架在你的處理程式跑起來之前就先把主體解析成物件了(JSON 中介軟體、表單解析等),你已經丟失了原始位元組序列——請改用原始 body 讀取器來註冊這些回呼路由。範例見Session 撤銷回呼裡用 express.text({ type: '*/*' }) 寫的 Express 範例。