数据验证
Mini App 传递给服务端的初始化数据(initData)需要进行签名验证,确保数据来源可信且未被篡改。
HMAC-SHA256 验证(服务端)
这是 Bot 服务端验证 initData 的标准方法。
验证步骤
- 从
initData查询字符串中提取hash字段,剩余字段按字母顺序排序 - 将排序后的字段用换行符
\n连接成data_check_string - 使用
"WebAppData"作为密钥,对 Bot Token 进行 HMAC-SHA256 运算得到secret_key - 使用
secret_key对data_check_string进行 HMAC-SHA256 运算 - 将结果与
hash字段进行比对
Python 示例
python
import hashlib
import hmac
from urllib.parse import parse_qs
def validate_init_data(init_data: str, bot_token: str) -> bool:
"""验证 Mini App 初始化数据"""
parsed = parse_qs(init_data)
# 提取 hash
received_hash = parsed.pop('hash', [None])[0]
if not received_hash:
return False
# 按字母顺序排序并构建 data_check_string
data_check_pairs = []
for key in sorted(parsed.keys()):
data_check_pairs.append(f"{key}={parsed[key][0]}")
data_check_string = "\n".join(data_check_pairs)
# 计算 secret_key
secret_key = hmac.new(
b"WebAppData",
bot_token.encode(),
hashlib.sha256
).digest()
# 计算并比对 hash
computed_hash = hmac.new(
secret_key,
data_check_string.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed_hash, received_hash)Node.js 示例
javascript
const crypto = require('crypto');
function validateInitData(initData, botToken) {
const params = new URLSearchParams(initData);
const hash = params.get('hash');
params.delete('hash');
// 按字母顺序排序
const dataCheckString = Array.from(params.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join('\n');
// 计算 secret_key
const secretKey = crypto
.createHmac('sha256', 'WebAppData')
.update(botToken)
.digest();
// 计算并比对 hash
const computedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex');
return computedHash === hash;
}Go 示例
go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/url"
"sort"
"strings"
)
func ValidateInitData(initData, botToken string) bool {
values, err := url.ParseQuery(initData)
if err != nil {
return false
}
receivedHash := values.Get("hash")
values.Del("hash")
// 按字母顺序排序
keys := make([]string, 0, len(values))
for k := range values {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([]string, 0, len(keys))
for _, k := range keys {
pairs = append(pairs, k+"="+values.Get(k))
}
dataCheckString := strings.Join(pairs, "\n")
// 计算 secret_key
mac := hmac.New(sha256.New, []byte("WebAppData"))
mac.Write([]byte(botToken))
secretKey := mac.Sum(nil)
// 计算并比对 hash
mac2 := hmac.New(sha256.New, secretKey)
mac2.Write([]byte(dataCheckString))
computedHash := hex.EncodeToString(mac2.Sum(nil))
return hmac.Equal([]byte(computedHash), []byte(receivedHash))
}Ed25519 验证(第三方)
Bot API 8.0+ 引入了 Ed25519 签名验证,适用于第三方服务验证来自 Mini App 的数据。
平台公钥
整个平台使用唯一的 Ed25519 密钥对,所有 Bot 共享相同的公钥。第三方服务可以直接使用此公钥验证签名。
公钥(Hex):
你的公钥hex值验证步骤
- 从
initData查询字符串中提取signature字段(Base64 编码),剩余字段按字母顺序排序 - 构建
data_check_string,格式为:WebAppData\n{bot_id}\n{sorted_params} - 使用平台 Ed25519 公钥验证签名
重要:initData 本身不包含 bot_id(只在签名计算中使用)。第三方服务需要从其他来源获取 bot_id(如 JWT、URL 参数、数据库等)。
Python 示例
python
import base64
from nacl.signing import VerifyKey
from urllib.parse import parse_qs
# 平台公钥(从上方复制)
PLATFORM_PUBLIC_KEY_HEX = "..."
def validate_init_data_ed25519(init_data: str, bot_id: int) -> bool:
"""
使用 Ed25519 验证 Mini App 初始化数据
参数:
init_data: Mini App 传递的 initData 字符串
bot_id: Bot ID(需要从其他来源获取,如 JWT、URL 参数等)
"""
parsed = parse_qs(init_data)
# 提取 signature
signature_b64 = parsed.pop('signature', [None])[0]
if not signature_b64:
return False
# 移除 hash(不参与签名)
parsed.pop('hash', None)
# 按字母顺序排序并构建参数字符串
data_check_pairs = []
for key in sorted(parsed.keys()):
data_check_pairs.append(f"{key}={parsed[key][0]}")
params_string = "\n".join(data_check_pairs)
# Telegram 格式:WebAppData\n{bot_id}\n{params}
data_check_string = f"WebAppData\n{bot_id}\n{params_string}"
try:
verify_key = VerifyKey(bytes.fromhex(PLATFORM_PUBLIC_KEY_HEX))
signature = base64.b64decode(signature_b64)
verify_key.verify(data_check_string.encode(), signature)
return True
except Exception:
return FalseNode.js 示例
javascript
const nacl = require('tweetnacl');
// 平台公钥(从上方复制)
const PLATFORM_PUBLIC_KEY_HEX = '...';
function validateInitDataEd25519(initData, botId) {
const params = new URLSearchParams(initData);
// 提取签名
const signatureB64 = params.get('signature');
if (!signatureB64) return false;
params.delete('signature');
params.delete('hash');
// 按字母顺序排序并构建参数字符串
const sortedKeys = Array.from(params.keys()).sort();
const dataCheckPairs = sortedKeys.map(key => `${key}=${params.get(key)}`);
const paramsString = dataCheckPairs.join('\n');
// Telegram 格式:WebAppData\n{bot_id}\n{params}
const dataCheckString = `WebAppData\n${botId}\n${paramsString}`;
try {
const publicKey = Buffer.from(PLATFORM_PUBLIC_KEY_HEX, 'hex');
const signature = Buffer.from(signatureB64, 'base64');
const message = Buffer.from(dataCheckString, 'utf8');
return nacl.sign.detached.verify(message, signature, publicKey);
} catch {
return false;
}
}安全建议
- 始终在服务端验证 — 不要仅依赖客户端的
initDataUnsafe - 检查 auth_date — 拒绝过期的初始化数据(建议有效期不超过 1 小时)
- 使用常量时间比较 — 使用
hmac.compare_digest(Python)或crypto.timingSafeEqual(Node.js)防止时序攻击 - 保护 Bot Token — 永远不要在客户端代码中暴露 Bot Token
javascript
// 服务端检查示例
function isInitDataValid(initData, botToken) {
if (!validateInitData(initData, botToken)) {
return false;
}
const params = new URLSearchParams(initData);
const authDate = parseInt(params.get('auth_date'));
const now = Math.floor(Date.now() / 1000);
// 检查数据是否在 1 小时内
if (now - authDate > 3600) {
return false;
}
return true;
}