跳至主要內容

在後端驗證 MiniApp 初始化資料(initData)

MPChat MiniApp initData 的後端 HMAC-SHA256 驗簽流程,含 Node.js 範例、推薦錯誤碼、TTL,以及目前不支援的回流能力。

先驗簽再信任。對排序後的欄位計算 HMAC-SHA256,常數時間比較,再校驗 auth_date 新鮮度。之後才讀取 user、chat 或 miniapp_id。


總覽

前端傳送原始 initData 字串。後端必須用 bot token 驗證其簽名後才能使用任何欄位。用戶資訊來自驗簽後的 initData.user;目前階段 MPChat 不簽發平台 access token。

驗簽步驟

  • 把 initData 當查詢串解析;取出 hash 並從簽名集合中移除。

  • 其餘欄位按 key 排序,格式化為 key=value 行,用換行連接成 data_check_string。

  • 密鑰 = HMAC-SHA256(key="WebAppData", message=bot token)。

  • 期望 hash = HMAC-SHA256(key=密鑰, message=data_check_string)。

  • 常數時間比較期望 hash 與收到的 hash。

  • 依伺服端的新鮮度策略檢查 auth_date。本範例接受 300 秒內的資料,允許 30 秒的未來時鐘偏差。

  • 若把請求綁定到某個 MiniApp,校驗簽名的 miniapp_id 是否相符。

Node.js 範例

從伺服端設定讀取 Bot token 和預期的 MiniApp ID。拒絕重複欄位,對除 hash 外的所有已解碼欄位驗證簽章,通過簽章與時間檢查後再解析 user。有效期內的簽章仍可重送,一次性業務操作需另外去除重複。

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyInitData(initData, botToken, { miniappId } = {}) {
if (typeof initData !== "string" || !initData || !botToken) {
throw new Error("INIT_DATA_INVALID");
}
const params = new URLSearchParams(initData);
if (new Set(params.keys()).size !== params.size) {
throw new Error("INIT_DATA_INVALID");
}

const hash = params.get("hash");
const authDateValue = params.get("auth_date");
if (!/^[a-f0-9]{64}$/i.test(hash ?? "") || !/^\d+$/.test(authDateValue ?? "")) {
throw new Error("INIT_DATA_INVALID");
}

params.delete("hash");
params.sort();
const checkString = [...params].map(([key, value]) => key + "=" + value).join("\n");
const secret = createHmac("sha256", "WebAppData").update(botToken).digest();
const expected = createHmac("sha256", secret).update(checkString).digest();
if (!timingSafeEqual(Buffer.from(hash, "hex"), expected)) {
throw new Error("INIT_DATA_INVALID");
}

const authDate = Number(authDateValue);
const now = Math.floor(Date.now() / 1000);
const ttlSeconds = 300;
const futureSkewSeconds = 30;
if (!Number.isSafeInteger(authDate) || authDate < now - ttlSeconds ||
authDate > now + futureSkewSeconds) {
throw new Error("INIT_DATA_INVALID");
}
if (miniappId && params.get("miniapp_id") !== miniappId) {
throw new Error("MINIAPP_FORBIDDEN");
}

let user;
try {
user = JSON.parse(params.get("user"));
} catch {
throw new Error("INIT_DATA_INVALID");
}
if (!user || typeof user !== "object" || Array.isArray(user) || user.id == null) {
throw new Error("INIT_DATA_INVALID");
}
if (!(Number.isSafeInteger(user.id) && user.id >= 0) &&
!(typeof user.id === "string" && /^[0-9]+$/.test(user.id))) {
throw new Error("INIT_DATA_INVALID");
}
return user;
}

應用程式錯誤碼範例

情境

錯誤碼

initData 缺失/格式錯誤/被竄改/過期

INIT_DATA_INVALID

MiniApp 不存在

MINIAPP_NOT_FOUND

MiniApp 已停用

MINIAPP_DISABLED

miniapp_id 不屬於期望的 Bot/上下文

MINIAPP_FORBIDDEN

目前不支援

目前階段沒有完整的用戶端回流:web_app_data、sendData、answerWebAppQuery,以及平台 access token 交換。後端無需呼叫平台 Bearer-token 介面取得用戶資訊。

相關文章

錯誤回應與日誌中絕不能包含 bot token、完整 initData、資料庫 ID 或堆疊。

是否回答了您的問題?