簽名與身份驗證
運營商對接的兩個方向都沒有客戶端 SDK——你在自己的技術棧裡,於伺服端自行對呼叫平台的請求簽名,也自行校驗平台回調你的請求。兩個方向共用同一套 HMAC-SHA256 構造,不需要任何專有工具。
請求頭
每個簽名請求——無論是你呼叫平台,還是平台呼叫你——都帶四個請求頭:
| Header | Value |
|---|---|
X-Tenant-ID | 你的運營商租戶 ID |
X-Timestamp | 簽名時的 Unix 時間戳(秒) |
X-Nonce | 每次請求唯一的新鮮隨機值(例如 16 個隨機位元組,轉十六進位制) |
X-Signature | HMAC-SHA256(secret, method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body),轉十六進位制 |
method 是 HTTP 方法(POST/GET),body 是實際傳送的請求主體位元組(GET 時為空字串)。nonce 被直接納入簽名本身,而不只是作為裸請求頭傳送——這正是它能有效防重放的關鍵:校驗方會追蹤已經見過的 nonce 並拒絕重複值,而且因為 nonce 與簽名是密碼學綁定的,攻擊者無法在截獲的請求上換一個新的 nonce 來繞過這項檢查。
校驗方還會強制另外兩項要求:X-Timestamp 必須接近真實時間——精確的容許誤差見時鐘同步——而且每個 X-Nonce 只能使用一次。每種校驗失敗的具體響應方式見錯誤與重試。
path:依方向而定的一項例外
path 裡具體放什麼內容,取決於是誰在簽名:
- 你簽名呼叫平台的請求(運營商 API)——
path只是 URL 路徑,不含協定/主機/查詢字串,且必須和伺服端實際收到的完全一致。 - 平台簽名呼叫你的請求(錢包回調介面)——
path是完整的請求 URI,路徑和查詢字串。這正是為了覆蓋GET /v1/wallet/balance的playerRef查詢參數,讓它無法在傳輸途中被篡改而不使簽名失效。
每個運營商 API 呼叫也都會按租戶做速率限制——見錯誤與重試。
簽名你的請求(呼叫平台)
下面是一段 Node.js 的獨立示例,只用了內建的 crypto 模組——同樣的構造在任何具備 HMAC-SHA256 原語的語言裡都能實現。用它來簽名運營商 API 參考裡的每一個介面——完整範例見啟動遊戲會話。
import { createHmac, randomBytes } from 'node:crypto'
function signRequest(method, path, secret, body, tenantId) {
const timestamp = Math.floor(Date.now() / 1000).toString()
const nonce = randomBytes(16).toString('hex')
const mac = createHmac('sha256', secret)
mac.update(method)
mac.update('\n')
mac.update(path)
mac.update('\n')
mac.update(timestamp)
mac.update('\n')
mac.update(nonce)
mac.update('\n')
mac.update(body)
return {
'X-Tenant-ID': tenantId,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': mac.digest('hex'),
}
}校驗平台的簽名(錢包回調)
錢包回調介面是相反的方向——由平台呼叫你,用上面同樣的構造對完整 URI 簽名。這裡你是校驗方,不是簽名方——如果簽名對不上、時間戳超出你允許的時鐘偏差範圍、或缺少必要的請求頭,就應該拒絕該請求。這一側沒有 SDK 可用——你的錢包後端是你自己的實現,簽名校驗也需要你自己完成。它和上面的簽名範例其實是同樣幾行邏輯,只是反過來做校驗而不是產生簽名。
import { createHmac, timingSafeEqual } from 'node:crypto'
function verifyPlatformSignature(method, pathWithQuery, secret, body, headers, maxSkewSeconds = 300) {
const timestamp = headers['x-timestamp']
const nonce = headers['x-nonce']
const providedSignature = headers['x-signature']
if (!timestamp || !nonce || !providedSignature) return false
const skewSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp))
if (!Number.isFinite(skewSeconds) || skewSeconds > maxSkewSeconds) return false
const mac = createHmac('sha256', secret)
mac.update(method)
mac.update('\n')
mac.update(pathWithQuery) // 例如 `${req.path}?${queryString}`——必須包含查詢字串
mac.update('\n')
mac.update(timestamp)
mac.update('\n')
mac.update(nonce)
mac.update('\n')
mac.update(body) // 收到的原始位元組,解析 JSON 之前;GET 時為空字串
const expected = Buffer.from(mac.digest('hex'), 'hex')
const provided = Buffer.from(providedSignature, 'hex')
return expected.length === provided.length && timingSafeEqual(expected, provided)
}有兩個細節值得注意並做對:在任何 JSON 解析中介軟體執行之前讀取原始請求主體(簽名覆蓋的是實際傳送的位元組,而不是解析物件後重新序列化的版本),並且用常數時間比較(上面的 timingSafeEqual)而不是 === 來比較簽名,這樣時序側信道就不會洩漏猜測簽名命中了多少位元組。
取得你的共用密鑰
同一把密鑰用於雙向簽名——你用來簽名呼叫運營商 API 的密鑰,和平台用來簽名它對你的錢包回調的密鑰,是同一把。一個 secret,雙向共用。
它由平台管理團隊建立,在你的運營商租戶建立時透過帶外管道下發給你。查看或產生它都不是自助服務——沒有任何介面會把它回傳給你。但你可以透過你租戶的自助入口網站自行輪換它,以及修改平台發送錢包回調的目的地——見註冊你的錢包回調 URL。輪換後,舊密鑰會在一段寬限期內繼續有效,讓雙方有時間一起切換過去,而不是硬性斷點式的切換。