Реализация серверной верификации покупок (Receipt Validation)
Клиент прислал баг-репорт: пользователь купил Premium, получил токен транзакции, потом восстановил приложение с резервной копии на другом устройстве — и Premium снова активен без повторной оплаты. Классика. Причина — валидация только на клиенте: приложение проверяет локальный receipt или trust-флаг от StoreKit, не сверяясь с сервером.
Почему клиентская валидация — это не валидация
На iOS StoreKit 2 возвращает Transaction с подписью Apple. Можно верифицировать подпись локально через Transaction.verificationResult, но это не защищает от replay-атак: злоумышленник перехватывает валидный receipt одного пользователя и подставляет его в другой аккаунт. На Android ситуация аналогична — BillingClient.queryPurchasesAsync() возвращает Purchase объекты, которые клиент не должен трактовать как подтверждение без серверной проверки purchaseToken.
Самая частая схема мошенничества — «receipt sharing»: один receipt распространяется между пользователями через форумы. Без серверной базы, фиксирующей какой originalTransactionId (iOS) или orderId (Android) уже использован, это не поймать.
Как устроена нормальная серверная верификация
Сторона iOS (App Store Server API). Старый подход — POST на https://buy.itunes.apple.com/verifyReceipt с base64-encoded receipt-data — устарел. Apple продвигает App Store Server API v1: клиент передаёт серверу transactionId из Transaction.id (StoreKit 2), сервер делает GET /inApps/v1/history/{transactionId} с JWT-токеном (подписанным ES256 ключом из App Store Connect). Ответ — JWSTransaction, который нужно декодировать и верифицировать подпись через Apple Root CA.
Параллельно нужно подписаться на App Store Server Notifications V2: Apple пушит события (DID_RENEW, EXPIRED, REFUND, GRACE_PERIOD_EXPIRED) на ваш endpoint. Без этого статус подписки на сервере устаревает — пользователь отменил подписку, а у вас он ещё числится Premium.
Сторона Android (Google Play Developer API). Для разовых покупок — purchases.products.get с packageName, productId, purchaseToken. Для подписок — purchases.subscriptions.v2.get. Авторизация через Service Account с ролью Financial data viewer — это минимально необходимые права, не давайте Editor на весь проект. Ответ содержит purchaseState (0 = Purchased, 1 = Canceled, 2 = Pending) и acknowledgementState — если 0, нужно вызвать purchases.products.acknowledge, иначе Google вернёт деньги автоматически через 3 дня.
Идемпотентность и защита от replay. В базе храним таблицу purchase_receipts с уникальным индексом по original_transaction_id + product_id. При каждом запросе на верификацию сначала проверяем наличие записи — если уже верифицировали с другим user_id, отвечаем ошибкой. Это и есть защита от receipt sharing.
purchase_receipts
id uuid PK
user_id uuid FK
platform enum('ios','android')
original_transaction_id varchar UNIQUE (per product)
product_id varchar
purchase_state smallint
expires_at timestamptz -- для подписок
raw_payload jsonb -- оригинальный ответ от Apple/Google
verified_at timestamptz
Стек и интеграция
Серверная часть чаще всего Node.js (библиотека app-store-server-api) или Python (google-auth + googleapiclient). Для Node удобен пакет node-apple-receipt-verify для легаси-endpoint, но лучше сразу брать app-store-server-api от Apple — поддерживает JWT-авторизацию и верификацию JWS из коробки.
На стороне клиента iOS — минимум кода: получить Transaction.id из Transaction.all или из updates потока, отправить на бэкенд. Не передавайте весь appStoreReceiptURL — это легаси, и файл может быть невалидным на симуляторе.
На Android клиент передаёт purchaseToken и productId из Purchase.purchaseToken. Важно: токен может быть одним для нескольких productId при апгрейде подписки — учитывайте это в логике.
Процесс работы
Начинаем с аудита текущей схемы валидации — где именно проверяется receipt, есть ли серверная база покупок, обрабатываются ли Server Notifications. Дальше проектируем схему БД и API-эндпоинты, реализуем верификацию для каждой платформы, настраиваем webhook-обработчик для Server Notifications, покрываем тестами с моковыми ответами от Apple/Google. Отдельный этап — нагрузочное тестирование эндпоинта верификации, потому что при пиковых запусках (акция, фича в топе App Store) он получает всё разом.
Сроки — от 2 до 5 дней, зависит от наличия серверной инфраструктуры и количества типов покупок (разовые, подписки, consumable, non-consumable). Если сервер уже есть и нужно только добавить верификацию — ближе к 2 дням. Полная архитектура с нуля плюс миграция существующих пользователей — до 5.







