Что охватывает этот учебник
Когда открывается приложение Telegram Mini, клиент проходит initData в WebApp SDK. Ваш бэкэнд должен рассматривать эту строку как ненадежный вход: поддельный хэш дешево построить, если вы забудете какой-либо шаг. Официальный рецепт, опубликованный в Telegram docs:
1. Парсинг initData в виде строки запроса и отбрасывания хэшей. 2. Отсортируйте оставшиеся пары по ключу. 3. Соедините их с \n в data_check_string. 4. Вычислить HMAC-SHA-256(data_check_string, secret_key) где: secret_key = HMAC-SHA-256("WebAppData", bot_token). 5. Сравните шестигранный дайджест с хэшей используя безопасное по времени сравнение. 6. Убедитесь, что Дата авторизации находится в пределах допустимого окна.
Я пройдусь по одному validateInitData которая делает именно это, а затем показывает, как подключить его к контроллеру. Я not повторное воспроизведение подписи обложки в разных ботах, API квитанции или виджете входа в систему — это разные поверхности с разными правилами.
Просмотр initData
initData поставляется в виде строки запроса с процентным кодированием. parse_str работает, но он преобразует точки в ключах в подчеркивания, что ломает такие ключи, как tg_start_param на PHP 8+, если parse_str работает без устаревшего поведения. Самый простой путь: использование urldecode после разделения на &.
function parseInitData(string $raw): array
{
$pairs = [];
foreach (explode('&', $raw) as $pair) {
if ($pair === '') {
continue;
}
$kv = explode('=', $pair, 2);
if (count($kv) !== 2) {
continue;
}
$pairs[urldecode($kv[0])] = urldecode($kv[1]);
}
return $pairs;
}
Это даст вам плоский массив. хэшей присутствует на верхнем уровне наряду с такими полями, как Дата авторизации, Сортировать по ID, пользователя, start_param, и chat_type. Вложенные поля, такие как пользователя поступают как JSON; мы оставляем их закодированными до тех пор, пока не будем готовы к декодированию, потому что HMAC вычисляется по учетом значения.
Building data_check_string
Спецификация Telegram явная: каждая оставшаяся пара входит значение ключа форма, отсортированная по ключу, объединенная \n. Сохраняйте значения точно такими, какими они пришли после URL-декодирования — без перекодирования JSON.
function buildCheckString(array $params): string
{
unset($params['hash']);
ksort($params, SORT_STRING);
$lines = [];
foreach ($params as $k => $v) {
$lines[] = $k . '=' . $v;
}
return implode("\n", $lines);
}
Тонкий источник ошибок ksort флажки Сортировка соответствует официальным примерам. Сортировки с учетом локали (по умолчанию в некоторых сборках PHP используется определенный setlocale вызовы) изменит порядок букв и сломает хэш.
Получение секретного ключа
Ключ HMAC not самого токена бота. Это HMAC-SHA-256("WebAppData", bot_token), возвращается в виде необработанных байтов. В документах Telegram и в источнике WebApp используется эта конструкция; если вы пропустите вложенный HMAC, каждая проверка подписи будет неудачной.
function webAppSecretKey(string $botToken): string
{
return hash_hmac('sha256', "WebAppData", $botToken, true);
}
Испытание пройдено true в качестве последнего аргумента, чтобы получить необработанный 32-байтовый дайджест, а затем загрузить его в hash_hmac снова для строки проверки данных.
Сравнение по времени и auth_date TTL
Равнина === между двумя шестнадцатеричными строками протекает длина префикса через разницу во времени. Используйте hash_equals:
function verifyInitData(string $raw, string $botToken, int $maxAgeSeconds = 86400): array
{
$params = parseInitData($raw);
if (!isset($params['hash'], $params['auth_date'])) {
return ['ok' => false, 'reason' => 'missing_fields'];
}
$checkString = buildCheckString($params);
$secretKey = webAppSecretKey($botToken);
$computed = hash_hmac('sha256', $checkString, $secretKey);
if (!hash_equals($params['hash'], $computed)) {
return ['ok' => false, 'reason' => 'bad_signature'];
}
$authDate = (int) $params['auth_date'];
if ($authDate <= 0 || $authDate > time() + 60) {
return ['ok' => false, 'reason' => 'auth_date_in_future'];
}
if (time() - $authDate > $maxAgeSeconds) {
return ['ok' => false, 'reason' => 'auth_date_expired'];
}
$user = isset($params['user']) ? json_decode($params['user'], true) : null;
return [
'ok' => true,
'user_id' => is_array($user) && isset($user['id']) ? (int) $user['id'] : null,
'user' => $user,
'params' => $params,
];
}
Несколько продуманных вариантов:
- Я добавляю небольшой +60-секундный перекос к верхней границе. Дрейф часов между клиентом Telegram и вашим сервером небольшой, но реальный; отклонение только что отчеканенного initData потому что это «1,2 секунды в будущем» - это тип ошибки, которая тратит день впустую. - TTL по умолчанию - 24 часа. Чем короче, тем безопаснее (15 минут - обычная производственная стоимость), чем дольше, тем дружелюбнее для оффлайн-спа-сессий. Выберите то, что терпит ваша модель угроз. - Декодирование JSON стороне. пользователя поле готово через проверка подписи. Если JSON неверен, вы все равно знаете, что запрос был подлинным; вы просто не можете доверять вложенным полям.
Подключение его к контроллеру
Публикации клиента Mini App initData либо RAW (в стандартном WebApp SDK), либо в виде поля формы. Относитесь к сырой ценности как к источнику истины. Чтение токена бота из конфигурации, никогда из ввода запроса.
$token = getenv('TELEGRAM_BOT_TOKEN');
if ($token === false || $token === '') {
http_response_code(500);
exit('bot token not configured');
}
$raw = $_POST['initData'] ?? '';
if ($raw === '') {
http_response_code(400);
exit('initData missing');
}
$result = verifyInitData($raw, $token, maxAgeSeconds: 900);
if (!$result['ok']) {
http_response_code(401);
exit('invalid: ' . $result['reason']);
}
// result['user_id'] is now safe to bind to a session or DB row
Две заметки об упрочнении, которые легко забыть:
- Ничего не кэшировать на основе хэшей в покое. А. Подписанные initData доказывает, что клиент разговаривал с вашим ботом, но не авторизует конкретного пользователя по запросам. Привяжите к (bot_id, user_id) Серверная сторона Не вести лог initData. Содержит id, имя пользователя, а иногда start_param значения, которые действуют как одноразовые токены. Отредактируйте перед входом в систему.
Производственные заметки (необязательно)
Это вещи, которые я бы добавил, прежде чем этот код столкнется с реальным трафиком; они не требуются строго для самого шага проверки.
- Ограничение скорости конечной точки. Злоумышленник, который не может подделать подпись, все равно может спамить вашу конечную точку мусором. Token-bucket по IP и по tg_id как только он у вас будет. - Проверка вложенной формы пользователя. Даже после действительной подписи утверждайте, что ID utente является положительным целым числом, и это user.is_bot является ложным, если вы служите только людям. - Используйте стабильный источник часов. время подходит для проверки TTL, но если вы разветвляете проверку среди нескольких работников PHP-FPM за балансировщиком нагрузки с перекосом NTP, рассмотрите возможность чтения Дата авторизации против того же монотонного источника, который вы используете повсюду. - Наблюдайте для… can_send_after и chat_instance. Если ваше мини-приложение запускается с помощью встроенной кнопки в чате, эти поля отображаются в initData. Ваша контрольная строка все еще работает — это просто дополнительные ключи, которые сортируются и хешируются, как и любые другие, — но вы можете заявить об их присутствии.
Если вам нужна эталонная реализация, которая также предоставляет типизированный DTO и интегрируется с хешем виджета входа, BotCreator поставляет ботов Telegram и мини-приложения из конца в конец. Справочник по API Telegram Bot - хороший компаньон при сборке: