先驗簽再信任。對排序後的欄位計算 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新鮮度(預設 TTL 300 秒)。若把請求綁定到某個 MiniApp,校驗簽名的
miniapp_id是否相符。
Node.js 範例
import { createHmac, timingSafeEqual } from "node:crypto";function buildCheckString(params) {
return [...params.entries()]
.filter(([k]) => k !== "hash")
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}=${v}`)
.join("\n");
}export function verifyInitData(initData, botToken, { ttlSeconds = 300, miniappId } = {}) {
const params = new URLSearchParams(initData);
const hash = params.get("hash");
const authDate = Number(params.get("auth_date"));
if (!hash || !Number.isFinite(authDate)) throw new Error("INIT_DATA_INVALID"); const secret = createHmac("sha256", "WebAppData").update(botToken).digest();
const expected = createHmac("sha256", secret).update(buildCheckString(params)).digest("hex"); const a = Buffer.from(hash, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) throw new Error("INIT_DATA_INVALID"); if (Math.floor(Date.now() / 1000) - authDate > ttlSeconds) throw new Error("INIT_DATA_INVALID");
if (miniappId && params.get("miniapp_id") !== miniappId) throw new Error("MINIAPP_FORBIDDEN"); return JSON.parse(params.get("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 或堆疊。
