Привет! Меня зовут Никита Улько, я fullstack-разработчик в Friflex . Мы разрабатываем мобильные приложения и нагруженные проекты для крупного бизнеса, и почти никогда в проектах не обходится без внедрения платежных систем. В этой статье разберем интеграцию платежной системы Stripe в Flutter приложении, а потом немного прогуляемся дальше в дикую природу, за пределы мобильных приложений. Я расскажу, как работают системы электронных платежей на примере GooglePay и ApplePay и как они связаны с платежными шлюзами. Разберем аспекты безопасности, которые важно держать в голове, и познакомимся со стандартом PCI DSS.

Современный пользователь привык иметь возможность оплаты в мобильном приложении. Он уже не задумывается, безопасно ли совершать онлайн-оплату, и как данные его банковской карты будут защищаться от злоумышленников во время совершения транзакции. Но если вы разработчик мобильных приложений или бэкенд сервисов, то вас не может не волновать этот вопрос. Опытный разработчик знает подводные камни интеграции с платежными системами, а также предвидит, какие последствия может нанести некачественная интеграция.

Stripe SDK для Flutter. Пример интеграции.

Начнем сразу с конкретного примера интеграции. Рассмотрим интеграцию оплаты при помощи новой карты. Stripe предоставляет официальный SDK для Flutter – https://pub.dev/packages/flutter_stripe.

Сначала нам нужно установить publishableKey для нашего приложения.

Stripe.publishableKey = 'YOUR_PUBLISHABLE_KEY';

В Stripe есть 2 основных типа ключей – publishableKey, который используется, например, в мобильном приложении, а также secretKey, который должен лежать на сервере. Заказы в Stripe (PaymentIntent) создаются именно на сервере с помощью secretKey.
Чтобы создать заказ, передадим детали заказа нашему бэкенду, который уже интегрировался с платежным шлюзом. Запрос может выглядеть, например, вот таким образом:

await _dio.request(
     '/order/',
     options: Options(method: HttpMethod.post),
     data: {
       'products': [
         {'productId': 1, 'amount': 2},
         {'productId': 14, 'amount': 1},
         {'productId': 4, 'amount': 11},
       ]
     },
   );

Обращу ваше внимание, что мы не передаем никаких ценников на бэкенд. Мы передаем лишь то, что мы хотим купить, а также, например, промокоды. Бэкенд сам определяет стоимость. Он и только он может знать, что и сколько должно стоить. В другом случае  злоумышленник может использовать модифицированный клиент и создать заказ с любой стоимостью.

В ответ на запрос бэкенд вернет вам детали только что созданного заказа. В нем будут различные данные, которые описывают этот заказ.  Все это произвольно и по вкусу. Самое важное, что вам вернет бэкенд, это client_secret:

{
   "client_secret": "pi_3M2cdHIeSIdLVBri2yWPzgXo_secret_XBrtcPcmxnaQlzxzkf7Su53IP",
   ...
}

client_secret – это уникальный ключ PaymentIntent. Он состоит из id PaymentIntent и секретной строки. Этот ключ позволяет клиенту при помощи него и publishableKey работать с объектом PaymentIntent. Именно по этому секрету клиент и будет проводить свои платежи в Stripe.

Для ввода данных карты мы воспользуемся Stripe Elements. Это набор виджетов, который спроектирован таким образом, что в коде вы даже физически не получите данные карты. А это хорошо! Мы не хотим их видеть, потому что они страшные. Разместим виджет CardField:

CardField(
 style: TextStyle(color: Colors.black),
)

В виджете можно указывать свои параметры стилизации, например, мы только что указали цвет текста. Как можно заметить, виджет не предусматривает никакого обмена данными с внешним миром. Мы не можем передать в него какой-либо обработчик событий ввода или контроллер. Внутри этот виджет отрисовывается как нативный компонент в зависимости от ОС. Редактируемое значение недоступно в нашем клиентском коде и устанавливается глобально в коде Stripe SDK. Этот виджет удобен тем, что он сам валидирует карточные данные, а также определяет вендора карты и отображает его иконку.

Предположим, пользователь ввел свои данные. Самое время отправить в Stripe запрос на подтверждение оплаты:

  try {
     final intent = await Stripe.instance.confirmPayment(
       clientSecret,
PaymentMethodParams.card(
         paymentMethodData: PaymentMethodData(),
       ),
     );


     return intent;
   } catch (error) {
     if (error is StripeException) {
       final errorMessage =
           error.error.message ?? CustomErrorObject.defaultErrorCaption;
       throw CustomErrorObject(errorMessage);
     }


     throw CustomErrorObject(CustomErrorObject.defaultErrorCaption);
   }

PaymentMethodParams в этом случае выступает просто в роли метки, означающей, что мы собираемся оплатить при помощи данных карты, которую ввели через Stripe Elements. StripeException содержит подробную информацию о провалившемся платеже. Из него вы сможете достать сообщение, например, о недостаточном балансе карты или ошибке прохождения 3dsecure.

Метод confirmPayment возвращает объект PaymentIntent. Из него вы сможете получить данные заказа: стоимость, валюту и, что самое важное, статус.

Одно из удобств Stripe – автоматическая обработка 3DSecure. При вызове confirmPayment, если требуется 3DSecure, Stripe SDK сам откроет страницу с вводом OTP кода. При этом Future будет висеть в pending state до тех пор, пока пользователь не разберется с 3DSecure. Это экономит массу времени.

Заметили ли вы что-нибудь? Мы ничего не узнали о данных карты пользователя! Они не коснулись серверов нашего бэкенда и даже нашего приложения на Flutter стороне. Мы делегировали безопасность этих конфиденциальных данных Stripe. Stripe сам получит эти данные после чего передаст их на свои сервера безопасным способом и будет использовать их для оплаты.

Если вам нужно сохранить карту для последующих оплат в приложении, вы можете создать PaymentMethod:

final paymentMethod = await Stripe.instance.createPaymentMethod(
       const PaymentMethodParams.card(
         paymentMethodData: PaymentMethodData(),
       )
 );

Как можно заметить, в обоих случаях используется объект PaymentMethodParams. Именно с его помощью мы передаем данные платежного средства (банковской карты или токена GooglePay).

Еще один способ ввести данные карты

Допустим, вам по какой-то причине не подходит Stripe Elements, и вы хотите вводить данные карты в своем собственном виджете. Вы можете воспользоваться методом dangerouslyUpdateCardDetails:

     Stripe.instance.dangerouslyUpdateCardDetails(
       CardDetails(
         number: '4242424242424242',
         cvc: '123',
         expirationMonth: 12,
         expirationYear: 25
       ),
     );

Вызов этого метода также обновит данные карты в глобальном объекте Stripe SDK. Но вам придется досконально убедиться в том, что вы не логируете данные карты по ошибке, нигде их не сохраняете и никуда не передаете.

Ниже представлена диаграмма последовательностей для создания заказа и платежного средства.

Создание заказа:

Создание платежного средства:

Stripe. А что мы вообще можем с его помощью?

В этом разделе мы уже уходим от привычного Flutter окружения. Как же создать PaymentIntent в Stripe? В общем, вы можете полностью экспериментировать со Stripe как душе угодно при помощи «Test mode» и http клиента. Если вы находитесь на стадии разработки, а у бэкенда проблемы с интеграцией, в то время как ваши сроки поджимают, можно отправлять запросы через curl и тестировать вашу интеграцию таким образом. В официальной документации API Stripe есть примеры запросов с использованием curl. Пример создания PaymentIntent:

curl -X POST https://api.stripe.com/v1/payment_intents \
 -u $SECRET_KEY
 -d amount=2000 \
 -d currency=usd \
 -d "payment_method_types[]"=card

Ответ:

{
 "id": "pi_3M9XDnIeSIdLVBri2QhaeCyl",
 "object": "payment_intent",
 "amount": 2000,
 ...
 "client_secret": "pi_3M9XDnIeSIdLVBri2QhaeCyl_secret_iTY9NP9CuAW9sNbqdDBJ3hAz0",
 ...
 "currency": "usd",
 ...
}

Secret key позволяет вам запрашивать данные PaymentIntent даже без client_secret:

curl -X GET https://api.stripe.com/v1/payment_intents/pi_3M2cdHIeSIdLVBri2yWPzgXo \
  -u $SECRET_KEY


# Ответ
{
   "id": "pi_3M2cdHIeSIdLVBri2yWPzgXo",
   "object": "payment_intent",
  "amount": 2000,
   ...
   "currency": "usd",
   ...
}

Самые важные сущности в Stripe – PaymentIntent и PaymentMethod. PaymentMethod – это любое платежное средство, которым можно осуществить оплату. Stripe поддерживает множество различных средств. Например, можно сохранить карту, оплатить при помощи ApplePay, GooglePay, Kiwi кошелька. Чтобы заплатить при помощи PaymentMethod, вам потребуется id этого PaymentMethod, который вы получите при создании. Этот токен даже не содержит данных карты, поэтому бесполезен для потенциального злоумышленника (тем не менее эти токены все равно нужно хранить в безопасном месте, как сказано в документации Stripe).

Также стоит упомянуть о сущности Customer. К Customer можно привязывать PaymentMethods для защиты. Привязанный PaymentMethod можно использовать только для оплаты заказов, выставленных тому же Customer, к которому привязан PaymentMethod. Давайте создадим покупателя и привяжем к нему платежное средство:

curl https://api.stripe.com/v1/customers \
 -u $SECRET_KEY \
 -d description="Test customer"
# cus_MthUiCmgGHkVlN


curl https://api.stripe.com/v1/payment_methods \
 -u $SECRET_KEY \
 -d type=card \
 -d "card[number]"=4242424242424242 \
 -d "card[exp_month]"=11 \
 -d "card[exp_year]"=2024 \
 -d "card[cvc]"=313
# pm_1M9vBTIeSIdLVBri2X5xuNBQ


curl https://api.stripe.com/v1/payment_methods/pm_1M9vBTIeSIdLVBri2X5xuNBQ/attach \
 -u $SECRET_KEY \
 -d customer=cus_MthUiCmgGHkVlN

Только что созданное платежное средство может быть использовано всего один раз (для оплаты любого заказа, в том числе заказа любого покупателя). Для совершения последующих платежей нужно привязать платежное средство к покупателю. Платежным средством покупателя можно оплачивать только заказы, которые привязаны к этому же покупателю. Анонимные заказы оплатить не получится. Хорошая практика сразу привязывать PaymentMethod к Customer после создания, если вы собираетесь использовать этот PaymentMethod в будущем (пример – сохранение карты в приложении).

Системы электронных платежей. Пример интеграции.

Системы электронных платежей – это всем знакомые ApplePay и GooglePay. Вы сохраняете карту, например, в GooglePay  и можете оплачивать покупки без ввода данных.

Как это работает? При выборе карты в нативном попапе GooglePay вы получаете токен этой карты. Этот токен многоразовый, но не используется нами (мерчантом) напрямую. Мы передаем его в Stripe, взамен получая PaymentMethod, либо сразу подтверждаем PaymentIntent с его помощью.

Для работы с системами электронных платежей пригодится pay плагин. Flutter Stripe SDK поддерживает его и позволяет обрабатывать токены. Вне зависимости от выбранного вами платежного шлюза  вам придется настроить payment profiles. Payment profile – это json, который хранит в себе ваш MerchantId, а также может содержать другие необходимые данные платежного шлюза в произвольном формате. Кроме того можно указывать, какие сети карт вы используете (visa, mastercard) и другие параметры.

Так может выглядеть payment profile для использования GooglePay совместно со Stripe:

{
   "provider": "google_pay",
   "data": {
     "environment": "TEST",
     "apiVersion": 2,
     "apiVersionMinor": 0,
     "allowedPaymentMethods": [
       {
         "type": "CARD",
 "tokenizationSpecification": {
           "type": "PAYMENT_GATEWAY",
           "parameters": {
             "gateway": "stripe",
             "stripe:version": "2018-10-31",
             "stripe:publishableKey": "***"
           }
         },
         "parameters": {
           "allowedCardNetworks": ["VISA", "MASTERCARD"],
           "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"],
           "billingAddressRequired": true,
           "billingAddressParameters": {
             "format": "FULL",
             "phoneNumberRequired": true
           }
         }
       }
     ],
     "merchantInfo": {
       "merchantId": "***",
       "merchantName": "My merchant name"
     },
     "transactionInfo": {
       "countryCode": "US",
       "currencyCode": "USD"
     }
   }
 }

И для ApplePay:

{
 "provider": "apple_pay",
 "data": {
   "merchantIdentifier": "merchant.ru.mycompany.myapp.ios",
   "displayName": "My app",
   "merchantCapabilities": ["3DS", "debit", "credit"],
   "supportedNetworks": ["amex", "visa", "discover", "masterCard"],
   "countryCode": "US",
   "currencyCode": "USD",
   "requiredBillingContactFields": [
   "emailAddress",
     "name",
     "phoneNumber",
     "postalAddress"
   ],
   "requiredShippingContactFields": [],
   "shippingMethods": []
 }
}

Обязательно указываем эти профили в ассетах pubspec.yaml:

assets:
   - assets/payment_profile_google_pay.json
   - assets/payment_profile_apple_pay.json

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

После настройки payment profiles можно приступать непосредственно к написанию кода. В первую очередь нужно загрузить профили.

GetIt.instance.registerSingleton<Pay>(
   Pay.withAssets([
     'payment_profile_google_pay.json',
     'payment_profile_apple_pay.json',
   ]),
 );

У pay плагина нет какого-то глобального объекта для хранения конфигурации, поэтому объектом плагина вам придется распоряжаться самостоятельно. Например, можно зарегистрировать его как Singleton, как показано выше.

Плагин позволяет проверить системы электронных платежей, доступные на устройстве пользователя:

 googlePayAvailable = await payClient.userCanPay(PayProvider.google_pay);
 applePayAvailable = await payClient.userCanPay(PayProvider.apple_pay);

В зависимости от доступных систем  вы выбираете предпочитаемую. Хотя у нас обычно присутствует только одна из двух,  никто не отменяет возможности появления других систем, которые могут вместе существовать на устройстве пользователя, не конфликтуя (или, например, может быть устройство, на котором не доступна ни одна из перечисленных).
Наконец, запрашиваем системный попап с GooglePay:

 final result = await pay.showPaymentSelector(
     provider: PayProvider.google_pay,
     paymentItems: [
       PaymentItem(
         label: 'Total',
         amount: '99.99',
         status: PaymentItemStatus.final_price,
       )
     ],
   );


   final token = result['paymentMethodData']['tokenizationData']['token'];


   final tokenJson = Map.castFrom(jsonDecode(token));
   // здесь в игру вступает Stripe SDK
   final params = PaymentMethodParams.cardFromToken(
     paymentMethodData: PaymentMethodDataCardFromToken(
       token: tokenJson['id'],
     ),
   );

Stripe SDK метод PaymentMethodParams.cardFromToken создает знакомый нам PaymentMethodParams объект с GooglePay токеном. Дальше этот объект можно использовать для оплаты или создания платежного средства, как было показано ранее в случае, когда оплата происходила по карте.

Заключение

В статье был рассмотрен пример интеграции платежного шлюза Stripe во Flutter приложении. 

Основные аспекты безопасности, на которые нужно обращать внимание:

  • Данные карты. Вы должны обращаться с ними максимально аккуратно. Никогда не передавайте их через ваш API на сервер ( конечно, если не хотите заполнять SAQ D самостоятельно и проводить ASV сканирование). В идеале вы не должны иметь к ним никакого доступа (как в случае со Stripe Elements, к примеру).

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

  • Цены. Бэкенд не должен принимать никаких данных о стоимости товаров из приложения. Бэкенд сам и только сам определяет стоимость и выставляет счет.

  • Ключи. Для использования в приложении предназначен только публичный ключ. Убедитесь, что секретный ключ нельзя получить каким-либо образом с вашего сервера.

  • Обезличенные данные карты. Вы можете использовать не конфиденциальные данные карты, для того чтобы пользователь мог узнать свою карту и выбрать ту, которую он хочет использовать для оплаты сейчас. К таким данным относятся, например, последние 4 цифры номера карты, бренд карты.

  • Документация платежного шлюза. Пристально изучайте документацию вашего поставщика услуг. Зачастую там вы сможете найти предостережения по безопасности, а также рекомендации по интеграции, которые помогут вам минимизировать возможные риски.


Дополнение.PCI DSS. Что это, и почему вы не хотите иметь с этим ничего общего?

Про стандарт в общих чертах

PCI DSS – стандарт безопасности индустрии платёжных карт. Он утвержден крупнейшими платежными системами (Visa, MasterCard, American Express) и описывает требования, которые нужно соблюдать для обеспечения безопасности платежей. В терминологии разделяют двух основных участников процесса. ТСП (торгово-сервисное предприятие, мерчант, то есть мы) и поставщик услуг (платежный шлюз). Еще есть payment brand (visa, mastercard) и эквайер (банк).

Отдельно отмечу, что эта статья ссылается именно на стандарт версии 3.2.1. В 2022 г. Совет PCI представил стандарт версии 4.0, но полностью он вступит в силу только 31 марта 2025 г. Стандарт версии 4.0 выдвигает более жесткие требования к мерчантам.

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

  1. SAQ – лист самооценки. Их бывает несколько типов, например,
    SAQ A  для компаний, которые полностью делегировали обработку платежных данных третьей стороне (платежному шлюзу, который соответствует требованиям PCI DSS).
    SAQ A-EP  для компаний, которые полностью делегировали обработку платежных данных третьей стороне (платежному шлюзу, который соответствует требованиям PCI DSS), но могут повлиять. Часто это относится к сайтам с кастомными полями ввода для данных карты, поскольку могут быть использованы XSS атаки для фишинга платежных данных.

    SAQ D  для компаний, которые не подходят под критерии других типов SAQ. Например, ваш сервер принимает платежные данные.

    Поставщик услуг (платежный шлюз) может заполнять листы самооценки за вас, как это делает Stripe,  и вам не придется тратить на это время. Это относится к SAQ A, но не относится к SAQ D, который придется заполнять самостоятельно.

  2. ASV сканирование. ASV сканирование – это сканирование на предмет наличия уязвимостей, которое проводится сертифицированным вендором сканирования. В случае с SAQ A  вам не нужно проводить ASV сканирование. А вот SAQ D уже подразумевает проведение ежеквартальных сканов.

  3. QSA аудит. Аудит системы, который проводит независимая третья сторона. Как правило, применяется к мерчантам уровня 1.

Конфиденциальные данные

Стандарт определяет понятие «конфиденциальных данных». К ним относятся, например, данные магнитной ленты, cvc код, pin код. Согласно PCI DSS, эти данные можно передавать и обрабатывать (если вы, конечно, соответствуете стандарту), но их нигде нельзя хранить (даже ваш поставщик услуг должен их удалять). В случае если это необходимо, стандарт подразумевает обсуждение этого вопроса с брендом платежей. Стоит ли разбирать вопрос, что произойдет в случае, если какой-нибудь мерчант, не соответствующий стандарту, будет хранить эти данные у себя на сервере?

Стандарт также описывает данные, которые не являются конфиденциальными. К их использованию не выдвигается никаких особых требований. Вы можете их получать и обрабатывать, а также хранить у себя. К таким данным относятся, например, обезличенный номер карты (последние 4 цифры) или бренд карты. Эти данные даже полезно использовать, чтобы ваш пользователь мог визуально идентифицировать сохраненную карту в приложении.

Уровни мерчантов. Выше были упомянуты уровни мерчантов. Уровень мерчанта отражает его степень ответственности. Всего четыре уровня.  Уровень зависит от количества транзакций в год. Мерчант 4 уровня подразумевает минимум ответственности, мерчант 1 уровня – максимум. Мерчанты 1 уровня – это мерчанты, которые проводят более 6 миллионов транзакций в год. Для таких мерчантов не используются листы самооценки. Им обязательно привлекать компанию аудитора для подтверждения своего соответствия PCI DSS.

Риски

Несоответствие стандарту PCI DSS может повлечь штраф от компаний, предоставляющих услуги электронных платежей. Также предусматривается штраф за утечки данных. Помимо всего прочего несоответствие стандарту может привести к судебным разбирательствам против вашей компании.

Источники

  1. A guide to PCI compliance

  2. Security at Stripe. Integration security guide

  3. Cloudpayments / Инфраструктура / PCI DSS

  4. FAQ: Can card verification codes/values be stored for card-on-file or recurring transactions?

  5. PCI DSS стандарт 3.2.1

  6. Stripe API Reference

  7. Cloudpayments. Оплата по криптограмме

  8. Payment Card Industry (PCI). Data Security Standard. Self-Assessment Questionnaire D and Attestation of Compliance for Merchants

  9. When are ASV scans required?

  10. Do or Do Not – ASV for SAQ A

  11. 5 CONSEQUENCES TO PCI NON-COMPLIANCE

P.S. Мы ведем дружелюбный канал про Flutter в Telegram. Присоединяйтесь!