本文へスキップ
Unveily
ブログ一覧へ

アプリ内課金をサーバー側で検証する:Google Play & App Store

著者 ActuallyWorks10 分で読めます
  • アプリ内課金
  • Google Play
  • App Store
  • セキュリティ

アプリでサブスクリプションや買い切りの商品を販売しているとします。ユーザーが支払いを終え、アプリにも「購入成功」という結果が返ってきました。さて、ここですぐに機能を開放してよいでしょうか。

そうすると問題が起きることがあります。アプリがサーバーに伝えてくる結果だけを信じると、改造されたアプリや root 化・脱獄された端末で、実際にはお金を払わずに「購入成功」と偽ることができるからです。そこで必要になるのがサーバー側での検証(server-side verification)です。

まず、サーバー検証とは?

アプリで購入が終わると、Google Play や App Store が、購入を確認できる情報をアプリに渡します。しかし、アプリがその情報だけを確認して有料機能を開放するのは安全ではありません。

代わりに、アプリは購入情報を自分のサーバーへ送り、サーバーが改めて Google や Apple に「本当に支払われたのか」を確認します。

やさしく言うと:

アプリ
   ↓  「購入しました」
自分のサーバー
   ↓  「Google / Apple、本当に購入されたものですか?」
Google / Apple

この一往復が、サーバー側の購入検証です。では、この流れを Google Play と App Store でそれぞれどう実装するか見ていきましょう。

基本の流れ

Google Play も App Store も、大きな流れは似ています。

  1. ユーザーがアプリ内で購入します(Android は Google Play Billing、iOS は StoreKit)。
  2. アプリが購入情報をサーバーへ送ります — Android は主に購入トークン(purchase token)、iOS はトランザクション情報を渡します。
  3. サーバーが Google または Apple の API を呼び出し、本当の購入かを確認します。
  4. 正常な購入で、まだ処理していないものなら、ユーザーに買ったもの(利用権限、entitlement)を付与します。「Pro アンロック」を買ったなら Pro 機能を有効にし、サブスクなら期間中サービスを使えるようにします。

大事なルールはひとつです。

クライアントを購入の最終的な証拠として信用しないこと。 アプリは購入情報を渡すだけで、最終判断は Google や Apple が確認した結果を基準にするのが安全です。

Google Play

サーバーが Google Play にアクセスする準備

まず、サーバーが Google Play に購入情報を問い合わせられる必要があります。そのために Google Cloud のサービスアカウント(Service Account)を使います — サーバーが Google Play API にアクセスするための専用アカウントです。

サービスアカウントに Play Console へのアクセス権を紐づけ、Google Play Developer API を有効化します。コードでは androidpublisher のスコープ(権限)を使います — サーバーが Google Play のアプリ・課金 API を使えるようにする権限です。

import { google } from "googleapis";

const auth = new google.auth.GoogleAuth({
  keyFile: "service-account.json",
  scopes: ["https://www.googleapis.com/auth/androidpublisher"],
});
const androidpublisher = google.androidpublisher({ version: "v3", auth });

サービスアカウントのキーファイルはサーバー内だけで管理してください。アプリに入れたり、公開リポジトリに上げたりしてはいけません。

買い切り商品を確認する

ユーザーが買い切り商品を購入すると、Google Play が購入トークン(purchase token)を発行します。アプリはこのトークンをサーバーへ渡し、サーバーは purchases.products.get API で Google Play に「この購入は本当に存在し、支払いは完了していますか?」と確認します。

const res = await androidpublisher.purchases.products.get({
  packageName: "com.example.app",
  productId: "pro_unlock",
  token: purchaseToken,
});

if (res.data.purchaseState === 0) {
  // 実際に購入が完了している場合
  grantEntitlement(); // 二重処理を防ぐため purchaseToken を保存しておく
}

purchaseState は購入の状態を表します。

  • 0 : 購入完了
  • 1 : キャンセル
  • 2 : 支払い保留中

支払いが保留中(pending)なら、まだ機能を開放してはいけません。実際に購入完了になってから利用権限を付与します。

確認のあとは acknowledge が必要です

Google Play では、購入を確認して機能を提供したあとに、もうひとつ作業が必要です。それが acknowledge です。Google に「この購入をきちんと処理しました」と伝える手続きで、ユーザーに再度支払いの承認を求める、という意味ではありません。

  • 非消費型の商品なら、サーバーで purchases.products.acknowledge を呼び出します。
  • 消費型の商品は、consume の処理をすると acknowledge も一緒に行われます。

3日以内に acknowledge しないと、Google Play がその購入を自動的に返金してしまうので、忘れないようにしましょう。

サブスクリプションは少し違います

サブスクは一度確認して終わりの決済ではありません。今日は正常でも、あとで状態が変わることがあります — 更新、支払い失敗、猶予期間、キャンセル、期限切れ、再登録などです。

サーバーでは purchases.subscriptionsv2.get で現在のサブスク状態を確認します。

const sub = await androidpublisher.purchases.subscriptionsv2.get({
  packageName: "com.example.app",
  token: purchaseToken,
});
// sub.data.subscriptionState を確認 — 有効・期限切れ・猶予期間 など

猶予期間(grace period)とは、サブスクの更新の支払いが失敗したものの、Google が再度支払いを試みる間、サブスクをすぐに止めずに一時的に維持してくれる期間です。ですから、サブスクは最初の決済のときに一度だけ確認して済ませてはいけません。

あとで状態が変わったら、どう知るのでしょう?

ユーザーがアプリを開いたときだけサブスク状態を確認すると、問題が起きます。ユーザーがアプリを使っていない間にも、サブスクが更新・キャンセルされたり、返金が発生したりするからです。

Google はこのために Real-Time Developer Notifications(RTDN)を提供しています — 購入やサブスクの状態が変わると、Google がサーバーに知らせてくれる機能です。この通知は Cloud Pub/Sub を通じて届きます — Google からサーバーまで通知を運んでくれるサービスです。

サブスク状態の変更

Google Play

RTDN

Cloud Pub/Sub

自分のサーバー

サーバーは通知を受け取ると、Google Play API で現在の状態を再確認し、ユーザーの利用権限を更新します。

App Store(iOS)

Apple も基本の原理は同じです。アプリが送ってきた決済結果だけを信じず、サーバーで Apple が提供する取引情報を確認します。ただし、Google とは使う方法と用語が少し違います。

StoreKit 2 と JWS

iOS アプリでアプリ内課金を実装するときは StoreKit 2 を使えます — iOS アプリの課金を担う Apple のフレームワークです。StoreKit 2 から得られる取引情報には、Apple の電子署名が含まれることがあります。

このときよく登場する用語が JWS(JSON Web Signature)です — Apple が電子署名した決済情報のことです。署名を確認できるので、アプリが作り出した偽の決済データではないかを見分けられます。

サーバーでは、大きく2つの方法で確認します。

  1. Apple が署名した取引情報を直接検証する、または
  2. App Store Server API を使って Apple に購入・サブスク状態を確認する。

App Store Server API の呼び出し

たとえば、特定のトランザクションの情報を照会できます。

GET https://api.storekit.itunes.apple.com/inApps/v1/transactions/{transactionId}
Authorization: Bearer <JWT>

ここで JWT(JSON Web Token)が登場します — サーバーが Apple API を使う権限があることを示す認証トークンです。App Store Connect で発行した API キーを使ってサーバーで JWT を作り、リクエストと一緒に送ります。Apple はこの JWT に ES256 という署名方式を使います — 「Apple API 用トークンに署名する方式」くらいの理解で十分です。アプリ内課金を作るために ES256 の暗号理論まで知る必要はありません。

Apple が返してきた JWS はどう確認する?

App Store Server API から受け取った取引情報にも、Apple の署名が含まれています。JWS の中身を読むと、次のような情報を確認できます。

  • どの商品を購入したか
  • いつ購入したか
  • サブスクがいつ期限切れになるか
  • 購入がキャンセル・返金されたか

そして、この署名が本当に Apple が作ったものかを確認する必要があります。ここで 証明書チェーン(certificate chain)が使われます — JWS の署名が本物の Apple のものかを検証するために使います。検証コードを自分で書くこともできますが、Apple が提供する App Store Server Library を使えば、JWS の検証と中身の確認をまとめて処理できます。

昔の verifyReceipt 方式は?

古い Apple のアプリ内課金の例を見ると、verifyReceipt という API がよく出てきます。以前は、アプリから受け取ったレシートデータをサーバーが Apple に送り、検証結果を受け取る方式がよく使われていました。この方式では2つの環境を区別します。

  • Production:実際の決済環境
  • Sandbox:テスト決済環境

昔の実装では、まず Production に送り、21007 が返ってきたら Sandbox に再リクエストしていました。

ただし、新しくアプリ内課金を実装するなら、この方式から始めるのはおすすめしません。Apple は verifyReceipt を deprecated(非推奨)とし、新しい実装では App Store Server API と Apple が署名した取引情報を推奨しています。既存システムを保守するときは知っておく必要がありますが、初めて作るなら最新の方式から見るのがよいでしょう。

App Store でも状態変更の通知を受け取れます

サブスクは iOS でも、時間とともに状態が変わり続けます — 更新、キャンセル、返金、支払い失敗などが、ユーザーがアプリを開いていない間にも起こります。これに対応するため、Apple は App Store Server Notifications V2 を提供しています — サブスクや決済の状態が変わると、Apple がサーバーに知らせてくれる機能で、Google の RTDN と目的は同じです。

このように、外部のサービスが何かが起きたときにサーバーへ自動で知らせる仕組みを、一般に Webhook(ウェブフック)と呼びます。

Google Play                    App Store
   ↓                              ↓
RTDN / Cloud Pub/Sub           App Store Server Notifications V2
   ↓                              ↓
自分のサーバー                   自分のサーバー

実装の方法は違いますが、目的は同じです。ユーザーがアプリを開かなくても、サーバーが最新の決済状態を把握している必要があります。

よくハマる落とし穴

1. Sandbox と Production を混同する。 テスト決済と実決済は別の環境です。テストデータを実購入として扱ったり、実決済をテスト環境で探そうとしたりすると、正常なユーザーがサービスを使えなくなることがあります。2つの環境をはっきり区別しましょう。

2. 同じ購入を二重に処理する。 同じ購入情報がサーバーに何度も届くことがあります — たとえばネットワークエラーで、アプリが同じ情報を再送する場合です。購入が確認されるたびに coins += 100 を実行すると、一度購入したユーザーが同じ情報で何度もアイテムを受け取れてしまいます。ですから、すでに処理した購入かを必ず確認してください。購入の識別情報 — Android は purchaseToken、iOS は transactionId — を保存して重複を防ぎます。Android で orderId だけを重複確認のキーにするのは避けましょう。すべての購入に orderId があるとは限らないためです。

3. Android で acknowledge を忘れる。 検証に成功したら終わり、ではありません。機能を提供したあと、Google にも購入処理が完了したと伝える必要があります — acknowledge の手順です。これを見落とすと、正常な購入があとで返金されることがあります。

4. サブスクを一度有効にすればずっと有効だと思い込む。 サブスクは一度きりのイベントではなく、状態が変わり続ける決済です。今日正常でも、来月も正常とは限りません。更新・期限切れ・返金・キャンセル・支払い失敗を反映し続ける必要があります。そうしないと、支払いをやめたユーザーに提供を続け、逆にちゃんと払っているユーザーの権限を止めてしまいます。

5. 端末の時刻を信じる。 サブスクの期限切れを、ユーザーのスマホの現在時刻だけで判断しないでください。ユーザーは端末の時刻を変えられます。Google や Apple で確認したサブスク状態と有効期限を基準に、サーバー側で判断するのが安全です。

結局、サーバーがやることはこうです

最初は単純に見えます。

購入成功

機能を開放

しかし実際のサービスでは、少し違います。

アプリで決済

購入情報

自分のサーバー

Google / Apple に本当の購入か確認

すでに処理した購入か確認

ユーザーに利用権限を付与

購入処理の完了

サブスクなら、ここで終わりではありません。

Google / Apple

更新 · キャンセル · 返金 · 支払い失敗

サーバーへの通知

サーバーのサブスク状態を更新

この構造を理解すれば、Google と Apple の API 名が違っても、全体の流れは難しくありません。

これは、作るのも保守するのも手間です

Google Play と App Store は、使う API も違い、サーバーの認証方法も違い、決済状態が変わったときの通知方法も違います。そこに、テスト/本番環境の区別、同じ購入の二重処理の防止、サブスク状態の管理、キャンセル・返金の処理、認証キーの管理までが必要になります。

自分で実装できない機能ではありません。しかし、一度作れば終わりのコードでもありません — Google や Apple のポリシーや API が変わると、保守し続けるバックエンドになります。

まさにこの部分を Unveily の IAP API が肩代わりします。ストアごとの決済バックエンドを自分で作らず、1つの API で購入検証と決済状態の処理をつなげられます。ストアごとのバックエンドを作って保守するより、アプリの機能づくりに集中したい — そんなときのためのものです。