← Назад в блог

Как принимать оплату через шлюз, оформленный на другой сайт

Опубликовано
3 мин чтения
--- просмотров

Иногда платёжный шлюз не ваш. Он принадлежит сайту-партнёру, другому юрлицу или конторе с лицензией, которой у вас нет. Покупателя приходится отправлять туда, а результат должен как-то вернуться обратно.

Такую интеграцию пишут плохо чаще, чем хорошо, и ломается она двумя конкретными способами. Их стоит назвать вслух.

Сбой первый: колбэк, который никто не аутентифицирует

Удалённая система должна сообщить вам, что оплата прошла, поэтому вы открываете эндпоинт. Если он принимает POST с номером заказа и статусом и действует по нему, то любой, кто узнал адрес, пометит любой заказ оплаченным. Магазин ему поверит, отгрузит товар и ничего не заметит.

Это не гипотеза. Адреса колбэков оседают в логах, в истории браузера, в обращениях поддержки, в JavaScript-бандлах.

Лечится двумя слоями, и нужны оба. Токен подтверждает, что вызывающему вообще разрешено вызывать. Подпись гарантирует, что тело запроса не переписали по дороге:

public function sign(string $body): string
{
    return hash_hmac('sha256', $body, $this->key);
}

public function verify(string $body, string $signature): bool
{
    return hash_equals($this->sign($body), $signature);
}

Здесь hash_equals, а не ===, потому что обычное сравнение строк выходит на первом же отличающемся байте, и потраченное время выдаёт, какая часть подписи была угадана верно. Это настоящая атака, а починка стоит одного имени функции.

Подпись должна покрывать сырое тело запроса, а не разобранный массив. Если сначала разбирать, а потом проверять, всё, что нормализует парсер, проходит без подписи.

Сбой второй: две записи о заказе, которые расходятся

Удалённый сайт создаёт под платёж свой заказ. Одна покупка теперь существует дважды, и эти две записи рано или поздно разойдутся, потому что расходится любая пара записей в двух системах.

Первый порыв - завести таблицу соответствий: локальный идентификатор, удалённый, обновляем при изменении любого. Работает до первой записи, упавшей на середине. После этого соответствие неверно, и заметить это нечем.

Лучше хранить связь на самих записях. Когда удалённый заказ создаётся, пометьте его исходным идентификатором и меткой источника, чтобы любая сторона нашла запись другой обычным запросом:

wc_get_orders([
    'meta_query' => [
        ['key' => '_order_id', 'value' => $wcOrderId],
        ['key' => '_source',   'value' => $orderProvider],
    ],
]);

Третьей сущности, которую надо синхронизировать, тут нет, поэтому и расходиться нечему. Связь становится свойством данных вместо записи о данных.

Метка источника важна не меньше идентификатора: два сайта-партнёра однажды пришлют вам обоим заказ номер 1041.

Логировать обмен, а не только результат

Каждый запрос и ответ в этом потоке должны попадать в таблицу: номер заказа, номер удалённого заказа, источник, статус, адрес колбэка, сырое тело.

Причина в спорах. Клиент говорит, что заплатил, вы говорите, что ничего об этом не слышали. Без лога это превращается в две команды, которые неделю вежливо не соглашаются, глядя каждая в свою панель. С логом получается запрос, и за минуту видно, приходил ли колбэк и что в нём было.

Сырое тело храните отдельно от разбора. Разобранная сводка говорит, что вы поняли, а сырое тело говорит, что вам прислали. Разрыв между этими двумя вещами и есть место, где живёт весь этот класс багов.

Передавать статус в обе стороны

Возврат или отмена на любой из сторон должны дойти до другой, иначе одна система тихо ошибается. Повесьте обработчик на смену локального статуса и отправляйте её наружу.

Именно этот кусок чаще всего пропускают, потому что счастливый путь работает и без него, а сбой всплывает недели спустя при сверке.

Общее правило

Когда деньги пересекают границу систем, считайте границу враждебной, а стороны - будущими спорщиками. Аутентифицируйте вызывающего, подписывайте тело, связывайте записи через их собственные поля и записывайте каждый обмен. Заранее это стоит недорого, а вкручивать задним числом, пока клиент ждёт ответа про свои деньги, невероятно дорого.

Открыт для работы по контракту

Я доступен для работы по контракту. Если у вас есть интересная идея проекта, запишитесь на звонок через Calendly.

Записаться на 30-минутный звонок