Как принимать оплату через шлюз, оформленный на другой сайт
- Опубликовано
- • 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-минутный звонок