先验签再信任。对排序后的字段计算 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 或堆栈。
