Що охоплює цей навчальний посібник
При відкритті програми Telegram Mini клієнт проходить initData в WebApp SDK. Ваш бекенд повинен розглядати цей рядок як ненадійний вхід: підроблений хеш дешево побудувати, якщо ви забудете будь-який крок. Офіційний рецепт опублікований в Telegram docs:
1. Парсинг initData як рядок запиту і скиньте хеші2. Відсортуйте решту пар за ключем 3. Підключіть їх до , n в data_check_string4. Розрахунок HMAC-SHA-256(Data_check_STRING, Secret_key) де: secret_key = HMAC-SHA-256("WebAppData", bot_token). 5. Порівняйте шістнадцятковий дайджест з хеші використовуючи безпечне порівняння за часом. 6. Переконайтеся, що Дата авторизації знаходиться в межах дозволеного вікна.
Я піду пішки один за одним validateInitData яка робить саме це, а потім показує вам, як підключити його до контролера. Я не повторне відтворення підпису обкладинки в різних ботах, 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;
}
Це дасть вам плоский масив. хеші присутній на найвищому рівні разом з такими полями, як Дата авторизації, Сортувати за, користувача., start_paramі chat_type. Вкладені поля, такі як користувача. діяти як JSON; ми залишаємо їх закодованими, поки не будемо готові декодувати, оскільки HMAC обчислюється за допомогою враховуючи Значення
Будівля 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 не самого маркера бота. Це HMAC-SHA-256("WebAppData", bot_token), повертається як RAW-байти. Документи 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 неправильний, ви все одно знаєте, що запит був справжнім; ви просто не можете довіряти вкладеним полям.
Підключення до контролера
Публікації клієнта міні-застосунку 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
Дві нотатки про загартовування, які легко забути:
- Не кешувати нічого на основі хеші поодинці. A. Підписано 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 - хороший компаньйон для збирання: