Skip to content

数据验证

Mini App 传递给服务端的初始化数据(initData)需要进行签名验证,确保数据来源可信且未被篡改。

HMAC-SHA256 验证(服务端)

这是 Bot 服务端验证 initData 的标准方法。

验证步骤

  1. initData 查询字符串中提取 hash 字段,剩余字段按字母顺序排序
  2. 将排序后的字段用换行符 \n 连接成 data_check_string
  3. 使用 "WebAppData" 作为密钥,对 Bot Token 进行 HMAC-SHA256 运算得到 secret_key
  4. 使用 secret_keydata_check_string 进行 HMAC-SHA256 运算
  5. 将结果与 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值

验证步骤

  1. initData 查询字符串中提取 signature 字段(Base64 编码),剩余字段按字母顺序排序
  2. 构建 data_check_string,格式为:WebAppData\n{bot_id}\n{sorted_params}
  3. 使用平台 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 False

Node.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;
}