Валидация данных
MAX передаёт стартовые параметры мини-приложению с каждым запуском. Чтобы убедиться, что данные принадлежат реальным пользователям и не были изменены, валидируйте их
Абстрактная логика проверки
Ниже описан общий алгоритм проверки объекта WebAppData. Реализуйте его на любом языке программирования
Вводные данные: BOT_TOKEN — токен бота, USER_URL — URL, по которому открыто мини-приложение
- Извлеките фрагмент из
USER_URL— данные после символа#. В нём в форматеkey=valueсодержатся параметры платформы, среди которых —WebAppData. Убедитесь, что каждый параметр встречается ровно один раз - Преобразуйте значение
WebAppDataизkey=valueв[['key', 'value']] - Убедитесь, что ключ
hashприсутствует в параметрах ровно один раз. Сохраните оригинальный хеш и исключите его из массива - Примените URL-декодирование для всех значений (
value), если ваша платформа не сделала этого автоматически - Отсортируйте массив по ключам в алфавитном порядке
a→z - Сформируйте строку:
key1=value1\nkey2=value2. Назовём еёlaunch_params - Вычислите
secret_key— ключ для проверки подписи. Для этого подпишите токен бота с помощью HMAC-SHA256, используя строкуWebAppDataв качестве ключа:HMAC-SHA256('WebAppData', BOT_TOKEN) - Вычислите собственную подпись: подпишите полученную на шаге 7 строку с помощью HMAC-SHA256, используя
secret_key. В итоге это будет выглядеть так:HMAC-SHA256(secret_key, launch_params) - Преобразуйте подпись из шага 8 в hex-строку
- Если hex-строка подписи равна значению параметра
hashиз оригинальной строки — данные подлинные
Валидация данных на стороне мини-приложения
Этап 1. Извлечение данных
При запуске мини-приложения клиентская часть получает закодированную строку для валидации данных через WebAppData
Извлеките параметры платформы из URL-фрагмента — данные после символа #. В параметре WebAppData содержатся данные для валидации. Эти данные также доступны через window.WebApp.initData
Пример URL с параметрами
https://example.com#WebAppData=chat%3D%257B%2522id%2522%253A12345%252C%2522type%2522%253A%2522DIALOG%2522%257D%26ip%3D192.168.0.1%26user%3D%257B%2522id%2522%253A67890%252C%2522first_name%2522%253A%2522Max%2522%252C%2522last_name%2522%253A%2522User%2522%252C%2522username%2522%253Anull%252C%2522language_code%2522%253A%2522ru%2522%252C%2522photo_url%2522%253Anull%257D%26query_id%3D4c0ab423-342b-4e45-aea4-2747dbc500cd%26auth_date%3D1771409719%26hash%3D<calculated_hash>&WebAppPlatform=web&WebAppVersion=26.2.8Этап 2. Подготовка данных
- Разбейте значение
WebAppDataпо символу&на парыkey=value - Сохраните значение параметра
hashи исключите его из дальнейшей обработки - Примените URL-декодирование ко всем значениям
- Отсортируйте параметры по ключам в алфавитном порядке
a→z - Сформируйте строку
launch_params, объединив парыkey=valueс разделителем\n(0x0A)
Пример подготовленных данных
auth_date=1771409719\nchat={"id":12345,"type":"DIALOG"}\nip=192.168.0.1\nquery_id=4c0ab423-342b-4e45-aea4-2747dbc500cd\nuser={"id":67890,"first_name":"Max","last_name":"User","username":null,"language_code":"ru","photo_url":null}
// hash
'<calculated_hash>'auth_date передаётся в формате Unix timestamp в секундах
Этап 3. Создание ключа шифрования
Создайте ключ шифрования secret_key с помощью алгоритма HMAC-SHA256. Используйте строку WebAppData в качестве ключа и токен бота, который вы получили при создании чат-бота на платформе MAX для партнёров:
HMAC_SHA256("WebAppData", Bot Token)
HMAC_SHA256(
"WebAppData",
"YOUR_BOT_TOKEN" // Bot Token
)Этап 4. Вычисление подписи
Вычислите подпись hash стартовых параметров initData. Подпишите строку launch_params с помощью HMAC-SHA256, используя secret_key в качестве криптографического ключа. Преобразуйте результат в hex-строку:
hex(HMAC_SHA256(secret_key, launch_params))
Этап 5. Сравнение результата
Сравните полученную hex-строку с оригинальным значением hash из WebAppData. Если подписи совпадают — данные подлинные. Если нет — данные были изменены
Примеры кода
// Вводные параметры
const BOT_TOKEN = 'YOUR_BOT_TOKEN';
const USER_LINK = 'https://example.com#WebAppData=...&WebAppPlatform=web&WebAppVersion=26.2.8';
// Извлекаем параметры платформы из фрагмента URL
const hashParams = new URLSearchParams(new URL(USER_LINK).hash.slice(1));
const appData: string = hashParams.get('WebAppData') || '';
const platform: string = hashParams.get('WebAppPlatform') || '';
const appVersion: string = hashParams.get('WebAppVersion') || '';
const validateAppData = async (appData: string, botToken: string): Promise<boolean> => {
// Преобразуем appData из key1=value1&key2=value2 в [["key", "value"], ["key2", "value2"]]
const params: string[][] = appData.split('&').map((x) => x.split('='));
// Если hash встречается больше одного раза — прерываем проверку
if (params.filter((x) => x[0] === 'hash').length !== 1) {
return false;
}
// Сохраняем хеш, который пришёл вместе с параметрами
const originalHash = params.find((x) => x[0] === 'hash');
// Если хеш отсутствует — валидация невозможна
if (!originalHash || typeof originalHash[1] !== 'string') {
return false;
}
// Производим URL-декодирование значений параметров
for (const param of params) {
param[1] = decodeURIComponent(param[1]);
}
// Сортируем параметры по названию ключа a -> z
params.sort((a, b) => a[0].localeCompare(b[0]));
// Формируем строку для подписи с разделителем \n, исключаем hash
const launchParams = params
.filter((x) => x[0] !== 'hash')
.map((x) => `${x[0]}=${x[1]}`)
.join('\n');
// Преобразуем строку для подписи и токен бота в массивы байтов
const encoder = new TextEncoder();
const botTokenBytes = encoder.encode(botToken);
const launchParamsBytes = encoder.encode(launchParams);
// Создаём secret_key: подписываем токен бота с помощью HMAC-SHA256,
// используя строку "WebAppData" в качестве ключа
const launchParamsKeyBytes = await crypto.subtle.sign(
'HMAC',
await crypto.subtle.importKey(
'raw',
encoder.encode('WebAppData'),
{
name: 'HMAC',
hash: {
name: 'SHA-256',
},
},
false,
['sign'],
),
botTokenBytes,
);
// Создаём подпись параметров с помощью HMAC-SHA256, используя secret_key
const signature = await crypto.subtle.sign(
'HMAC',
await crypto.subtle.importKey(
'raw',
launchParamsKeyBytes,
{
name: 'HMAC',
hash: {
name: 'SHA-256',
},
},
false,
['sign'],
),
launchParamsBytes,
);
// Переводим подпись из массива байтов в hex-формат
const hash = Array.from(new Uint8Array(signature))
.map(b => ('00' + b.toString(16))
.slice(-2))
.join('');
// Сравниваем с полученным хешем
return hash === originalHash[1];
};
console.log(await validateAppData(appData, BOT_TOKEN));import hmac
import hashlib
from urllib.parse import urlparse, parse_qsl, unquote
from operator import itemgetter
# Вводные параметры
BOT_TOKEN = 'YOUR_BOT_TOKEN'
USER_LINK = 'https://example.com#WebAppData=...&WebAppPlatform=web&WebAppVersion=26.2.8'
# Извлекаем параметры платформы из фрагмента URL
fragment_dict = dict(parse_qsl(urlparse(USER_LINK).fragment))
app_data = fragment_dict.get('WebAppData', '')
platform = fragment_dict.get('WebAppPlatform', '')
app_version = fragment_dict.get('WebAppVersion', '')
def validate_app_data(app_data: str, bot_token: str) -> bool:
"""
Проверяет валидность данных мини-приложения с помощью HMAC-SHA256.
"""
# Парсим строку данных (key1=value1&key2=value2...) в список кортежей
# parse_qsl автоматически выполняет URL-декодирование значений
params = list(dict(parse_qsl(app_data, keep_blank_values=True)).items())
# Ищем параметр hash и удаляем его из списка параметров для проверки
original_hash = next((value for key, value in params if key == 'hash'), None)
# Если хеш отсутствует — валидация невозможна
if not original_hash:
return False
# Формируем список параметров без hash, сортируем по ключу (a -> z)
params_to_sign = sorted([(k, v) for k, v in params if k != 'hash'], key=itemgetter(0))
# Формируем строку для подписи с разделителем \n, исключаем hash
launch_params = '\n'.join(f'{k}={v}' for k, v in params_to_sign)
# Создаём secret_key: подписываем токен бота с помощью HMAC-SHA256,
# используя строку "WebAppData" в качестве ключа
secret_key = hmac.new(
key=b"WebAppData",
msg=bot_token.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
# Вычисляем подпись WebAppData с помощью HMAC-SHA256, используя secret_key
hash = hmac.new(
key=secret_key,
msg=launch_params.encode('utf-8'),
digestmod=hashlib.sha256
).hexdigest()
# Сравниваем полученный хеш с пришедшим
return hmac.compare_digest(hash, original_hash)
print(validate_app_data(app_data, BOT_TOKEN))package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"sort"
"strings"
)
// Вводные параметры
const BotToken = "YOUR_BOT_TOKEN"
const UserLink = "https://example.com#WebAppData=...&WebAppPlatform=web&WebAppVersion=26.2.8"
func main() {
// Извлекаем фрагмент из URL (данные после #)
parts := strings.SplitN(UserLink, "#", 2)
if len(parts) < 2 {
panic("Invalid URL: no hash fragment found")
}
fragment := parts[1]
// Разбираем параметры фрагмента
var appDataEncoded string
fragmentParams := strings.Split(fragment, "&")
for _, p := range fragmentParams {
if strings.HasPrefix(p, "WebAppData=") {
// Если значение уже было записано, значит это дубликат
if appDataEncoded != "" {
panic("Duplicate WebAppData parameter found")
}
appDataEncoded = strings.TrimPrefix(p, "WebAppData=")
}
}
if appDataEncoded == "" {
panic("WebAppData parameter not found")
}
// Декодируем значение WebAppData
appData, err := url.QueryUnescape(appDataEncoded)
if err != nil {
panic(err)
}
// Запускаем проверку
isValid, err := ValidateAppData(appData, BotToken)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Print(isValid)
}
type paramPair struct {
key string
value string
}
func ValidateAppData(initData, botToken string) (bool, error) {
// Разбиваем строку по "&" на пары ключ=значение
pairs := strings.Split(initData, "&")
var params []paramPair
var originalHash string
// Перебираем параметры
for _, pair := range pairs {
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
continue
}
key := kv[0]
val := kv[1]
// Сохраняем оригинальный хеш и исключаем его
if key == "hash" {
// Если originalHash уже заполнен, значит встретили второй такой ключ
if originalHash != "" {
return false, fmt.Errorf("duplicate hash parameter found")
}
originalHash = val
continue
}
// Производим URL-декодирование
decodedVal, err := url.QueryUnescape(val)
if err != nil {
return false, fmt.Errorf("failed to decode val: %v", err)
}
params = append(params, paramPair{key: key, value: decodedVal})
}
// Если hash не найден — валидация невозможна
if originalHash == "" {
return false, fmt.Errorf("hash not found in initData")
}
// Сортируем параметры по алфавиту ключа (a -> z)
sort.Slice(params, func(i, j int) bool {
return params[i].key < params[j].key
})
// Формируем строку launch_params
var launchParamsList []string
for _, p := range params {
launchParamsList = append(launchParamsList, fmt.Sprintf("%s=%s", p.key, p.value))
}
launchParams := strings.Join(launchParamsList, "\n")
// Создаём secret_key: подписываем токен бота с помощью HMAC-SHA256,
// используя строку "WebAppData" в качестве ключа
h1 := hmac.New(sha256.New, []byte("WebAppData"))
h1.Write([]byte(botToken))
secretKey := h1.Sum(nil)
// Создаём подпись параметров с помощью HMAC-SHA256, используя secret_key
h2 := hmac.New(sha256.New, secretKey)
h2.Write([]byte(launchParams))
signature := h2.Sum(nil)
// Переводим в hex
hash := hex.EncodeToString(signature)
// Сравниваем
return hmac.Equal([]byte(hash), []byte(originalHash)), nil
}import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.stream.Collectors;
public class MaxSignValidator {
public static void main(String[] args) {
// Вводные параметры
String BOT_TOKEN = "YOUR_BOT_TOKEN";
String USER_LINK = "https://example.com#WebAppData=...&WebAppPlatform=web&WebAppVersion=26.2.8";
try {
// Извлекаем параметры платформы из фрагмента URL
String fragment = URI.create(USER_LINK).getRawFragment();
Map<String, String> fragmentParams = parseQueryString(fragment);
String appData = fragmentParams.getOrDefault("WebAppData", "");
String platform = fragmentParams.getOrDefault("WebAppPlatform", "");
String appVersion = fragmentParams.getOrDefault("WebAppVersion", "");
// Проверяем валидность WebAppData
System.out.println(validateAppData(appData, BOT_TOKEN));
} catch (Exception e) {
e.printStackTrace();
}
}
public static boolean validateAppData(String appData, String botToken)
throws NoSuchAlgorithmException, InvalidKeyException {
if (appData == null || appData.isEmpty()) {
return false;
}
// Преобразуем appData из key1=value1&key2=value2 в [["key", "value"], ...]
List<String[]> params = new ArrayList<>();
String[] pairs = appData.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
if (idx > 0) {
String key = pair.substring(0, idx);
String value = URLDecoder.decode(
pair.substring(idx + 1), StandardCharsets.UTF_8
);
params.add(new String[]{key, value});
}
}
// Находим оригинальный хеш
String originalHash = null;
for (String[] param : params) {
if ("hash".equals(param[0])) {
originalHash = param[1];
break;
}
}
// Если хеша нет — валидация невозможна
if (originalHash == null) {
return false;
}
// Сортируем параметры по алфавиту (a -> z)
params.sort(Comparator.comparing(a -> a[0]));
// Формируем строку для подписи с разделителем \n, исключаем hash
String launchParams = params.stream()
.filter(x -> !"hash".equals(x[0]))
.map(x -> x[0] + "=" + x[1])
.collect(Collectors.joining("\n"));
// Создаём secret_key: подписываем токен бота с помощью HMAC-SHA256,
// используя строку "WebAppData" в качестве ключа
byte[] secretKeyBytes = hmacSha256(
"WebAppData".getBytes(StandardCharsets.UTF_8),
botToken.getBytes(StandardCharsets.UTF_8)
);
// Создаём подпись параметров с помощью HMAC-SHA256, используя secret_key
byte[] signature = hmacSha256(
secretKeyBytes,
launchParams.getBytes(StandardCharsets.UTF_8)
);
// Переводим подпись в hex-формат
String hash = bytesToHex(signature);
// Сравниваем с полученным хешем
return hash.equalsIgnoreCase(originalHash);
}
private static byte[] hmacSha256(byte[] key, byte[] message)
throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
mac.init(secretKeySpec);
return mac.doFinal(message);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private static Map<String, String> parseQueryString(String query) {
Map<String, String> queryPairs = new LinkedHashMap<>();
if (query == null || query.isEmpty()) return queryPairs;
String[] pairs = query.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
String key = (idx > 0)
? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8)
: URLDecoder.decode(pair, StandardCharsets.UTF_8);
String value = (idx > 0 && pair.length() > idx + 1)
? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8)
: "";
if (queryPairs.containsKey(key)) {
throw new IllegalArgumentException("Duplicate parameter found: " + key);
}
queryPairs.put(key, value);
}
return queryPairs;
}
}
Если у вас возникли вопросы, [посмотрите раздел с ответами](/help)