Спойлер: кажется SHA1 устарел, а туториалы этого еще не знают

Привет всем.
Для контекста: я не программист. Я гуманитарий и до безумия боюсь цифрового неравенства, которое уже наступает. Поэтому, вооружившись AI‑агентом (Cursor) и бесплатными Claude, Gemini, DeepSeek, пытаюсь быть в тренде (некоторые называют это вайб‑кодингом). Признаться, я тоже думал, что это что‑то легкое, «вайбовое» — главное, всё внятно описать, что ты хочешь. Но по факту в том, что происходит, всё равно приходится разбираться самому.

Свежий пример моих «грабель» — решил прикрутить оплату ЮМани.
Казалось бы, ЮМани, всё придумано до меня. Но есть нюансы...

Что хотел сделать
Пользователь нажимает «Купить» → переходит на форму оплаты ЮМани → платит → ЮМани уведомляет мой сервер → сервер создаёт одноразовую ссылку на скачивание → пользователь получает файл. Все счастливы.

Насколько я понял и разобрался, то ЮМани шлет HTTP‑уведомления на мой сервер когда приходит платёж. Я указываю URL своего PHP‑скрипта в настройках кошелька — и при каждой оплате ЮМани делает POST‑запрос на этот адрес с данными о платеже.

То есть задача сводится к тому, что нужен скрипт, который принимает запрос, проверяет подпись (чтобы убедиться что это реально ЮМани, а не кто‑то левый), и если всё ок — записывает покупку в базу данных.

Пользователь тем временем попадает на страницу /success.php, нажимает «Проверить оплату» — страница опрашивает базу данных и если запись есть — выдаёт одноразовую ссылку на скачивание.

Проблема 1 — скрипт не отвечал

Когда я открыл адрес скрипта в браузере, получил ошибку 405 Method Not Allowed.

Паниковать еще рано! Оказалось, что это нормально — скрипт принимает только POST-запросы (которые шлёт ЮМани), а браузер открывает через GET. Сказал Cursor‑у добавить обработку GET с ответом «OK» — чисто для ручной проверки в браузере. ЮМани всегда шлёт POST и только POST.

Проблема 2 — уведомления доходят, но не записываются

После подсказки от Claude добавил логирование, чтобы смотреть по логам, что происходит. По логам уведомления приходят, но скрипт пишет «invalid sha1_hash». Это блин, что такое? Оказалось, что скрипт читал подпись из поля «sha1_hash», а ЮМани присылает в поле «sign» (зачем?!). Поменял название поля — в логе «received» что‑то появилось. Но хеши всё равно не совпадали.

Проблема 3 — неправильный алгоритм. Главная

Пока не полез в документацию ЮМани ничего не выходило. В общем оказалось, что я использовал в скрипте старый протокол... Скрипт считал подпись старым способом — SHA1 от строки параметров через & в фиксированном порядке.

А сейчас подпись — это HMAC‑SHA256 от URL‑кодированной строки всех параметров уведомления кроме sign. Параметры отсортированы по алфавиту.

То есть отличие от скрипта:
— Не SHA1, а HMAC‑SHA256 — принципиально другой алгоритм
— Параметры сортируются по алфавиту (раньше был фиксированный порядок)

Насколько я понял ЮМани обновили протокол — теперь только «sign» и HMAC‑SHA256.

Вот итоговый код проверки подписи:

// Берём все POST параметры кроме 'sign'
$params = $_POST;
$receivedSign = (string)($params['sign'] ?? '');
unset($params['sign']);
// Сортируем по алфавиту
ksort($params);
// Собираем строку key=urlencoded_value&key=urlencoded_value
$parts = [];
foreach ($params as $key => $value) {
    $parts[] = $key . '=' . rawurlencode((string)$value);
}
$hashString = implode('&', $parts);
// Считаем HMAC-SHA256 с секретным ключом из настроек ЮMoney
$calculatedSign = hash_hmac('sha256', $hashString, YOOMONEY_SECRET);
// Сравниваем через hash_equals (защита от timing attack)
if (!hash_equals($calculatedSign, $receivedSign)) {
    // Подпись не совпала — это точно не ЮMoney, можно смело 400
    http_response_code(400);
    exit;
}

Проблема 4 — card‑incoming

Но и это еще не все.
Провёл тестовый платёж картой. Деньги списались, но запись в БД не появилась.
Смотрю лог: YooMoney skip: notification rejected, type=card-incoming

Скрипт принимал только «p2p‑incoming» (перевод из кошелька), а оплата картой приходит как «card‑incoming». Попросил Cursor добавить оба типа:

$notification_type = $_POST['notification_type'] ?? '';
$allowedTypes = ['p2p-incoming', 'card-incoming'];
if (!in_array($notification_type, $allowedTypes)) {
    // Подпись верная — запрос точно от ЮМани.
    // Просто этот тип мы не обрабатываем (может быть новый тип в будущем).
    // Отвечаем 200, чтобы ЮМани не слала повторы и не считала сервер упавшим.
    http_response_code(200);
    exit;
}

Фух, блин, вроде заработало. Перевел сам себе 10 рублей. Победа!

В итоге, если кому понадобится — краткая инфо.

SHA1 в ЮМани не работает — теперь «sign» и HMAC‑SHA256. Если нашел инструкцию с «sha1_hash» и фиксированным порядком полей — он устарел.

«p2p‑incoming» и «card‑incoming» — надо оба, и карта, и кошелек.

Если подпись не совпала — отвечаем «400» (это чужой запрос, не ЮМани). Но если подпись верна, а тип платежа тебе просто не подходит — отвечаем «200», чтобы ЮМани не считала твой сервер упавшим и не отключила уведомления.

Логирование — при любом удобном случае. Нужен error_log в скрипте, чтобы реально смотреть, что приходит.

Тестовые уведомления от ЮМани не создают запись в БД — они только проверяют доступность скрипта и правильность подписи.

Ну, вот так...

Комментарии (3)


  1. vbatalov
    02.06.2026 15:19

    Почему это здесь, а не Пикабу?

    Скрытый текст

    !?


    1. MrSmitix
      02.06.2026 15:19

      По тому что пикабу уже здесь...

      ИИ написала статью (спасибо —) о том как без знании программирования через ИИ прикручивать эквайринг. С каждым днём мы всё дальше от бога. Спасибо что прошивки для мед оборудования ещё не вайбкодят.

      У них там в документации есть ещё operation_id и unaccepted из важных параметров которые не плохо было бы учитывать. Попросите агента добавить их обработку и напишите вторую часть. А ещё они уведомление присылают максимум 3 раза, так что если когда ваш сервис ляжет больше чем на час, а кому-то вздумается что-то купить, у вас будут проблемы. Это уже идея для 3 части


  1. SensDj
    02.06.2026 15:19

    вы как ИП к Юмани подключились ?