その他
    ホーム 職種別 エンジニア 月額課金のサーバー側レシート検証の実装
    月額課金のサーバー側レシート検証の実装
     

    月額課金のサーバー側レシート検証の実装

    今回は、アピリッツの知識共有サイト「ナレッジベース」で公開されている内容をアピスピでも紹介します。
    エンジニアとして活躍されている鳴澤秀夫さんの記事です。
    是非最後まで読んでください!(初版:2022年11月24日)

    某社某案件にて課金のレシート検証を最初から実装することになったので、そのうち月額課金に関して実装時に何をしたかなどをまとめます。


    課金購入の大まかな流れ

    大体の流れはこの記事で理解できるかと思います。

    月額課金の場合、これをベースに各工程で見るべきパラメータがが変わったり、月次更新の処理などが発生します。

    この記事ではそれら月額課金特有のケースについてのみまとめていきます。

    購入時

    購入(月額課金加入)時のレシート検証時に通常課金と月額課金では扱う値に多少の違いがあります。

    主にやることリスト

    1. (Androidのみ)レシート・署名・公開鍵を基にレシート整合性チェック
      • Googleのストアで発行された公開鍵を用いてレシートデータのsignatureが不正でないかをopensslを用いて判断する
    2. (Androidのみ)Googleのクライアント認証でアクセストークンを取得する(OAuth)
      • これをしないとストア側の検証APIを叩けない
    3. ストア側の検証APIを叩く
      • APIサーバーから詳細なレシートデータをjson形式で受け取る
    4. レシートパース
      • json形式で取得した詳細なレシートデータをパースして必要な情報を得る
    5. 重複チェック
      • 過去に扱ったレシートが不正に利用されたリクエストでないかをチェックする
    6. アイテム付与・DB保存
      • リクエストが月額課金だった場合、PJによっては専用の特典もここで別途付与する

    主に月額課金でやるべき項目が変わってくるのは、上記の3〜6の範囲です。

    Android編

    3. GooglePlayDeveloperAPIを叩く

    叩くべきURLが違います。若干違うだけでほとんど似てる文字列なので間違えないようにしましょう。
    私のPJでは購入したい商品が月額課金かどうかをif文で分岐させています。

    通常商品(都度購入)の場合
    https://androidpublisher.googleapis.com/androidpublisher/v3/applications/<パッケージ名>/purchases/products/<ストア側の商品ID>/tokens/<アクセストークン>
    
    月額課金の場合
    https://androidpublisher.googleapis.com/androidpublisher/v3/applications/<パッケージ名>/purchases/subscriptions/<ストア側の商品ID>/tokens/<アクセストークン>

    4. レシートパース

    月額課金で特に見るべきパラメータは以下の通りです

    expiryTimeMillis
    • そのレシートが月額課金である場合は上記パラメータがレシートのjson上に存在するはずなので、それの有無をまずは確認する。
    • 逆にそれが無い場合は都度課金と判断できます
    paymentState
    • 決済状態がint型で表され、0なら未決済、1なら購入完了となっています。
    • 都度課金の場合、全く同じ内容なのに purchaseState という別種のパラメータから取らなければならないので面倒な仕様です
    startTimeMillis
    • 月額課金のプラン開始日時がエポック秒かつミリ秒単位の数値で入っています。
      • なのでdatetime型にするためには秒数まで取れればいいので、変換前に1000で割った商を得ておきます。
    expiryTimeMillis

    プラン終了日時です。ソース上での扱いは上記と同じです

    iOS編

    3. In-AppPurchaseAPIを叩く

    iOSは都度課金・月額課金共にまとめて1つのレシートデータとして返してくるので課金形態でURLは変わりませんが、本番環境か否かでURLが変わる仕様なので注意しましょう

    本番
    https://buy.itunes.apple.com/verifyReceipt
    
    サンドボックス(本番以外)
    https://sandbox.itunes.apple.com/verifyReceipt

    4. レシートパース

    月額課金で特に見るべきパラメータは以下の通りです

    latest_receipt_info
    • 月額課金に関する情報はこのパラメータ下に配列で入っているため、まずはこれをまるごと取得してforeachなどで回します。
    expires_date
    • そのレシートが月額課金である場合は上記パラメータがレシートのjson上に存在するはずなので、それの有無をまずは確認する。
    • 逆にそれが無い場合は都度課金と判断できます
    purchase_date_ms
    • 月額課金のプラン開始日時がエポック秒かつミリ秒単位の数値で入っています。
      • なのでdatetime型にするためには秒数まで取れればいいので、変換前に1000で割った商を得ておきます。
    expires_date_ms

    プラン終了日時です。ソース上での扱いは上記と同じです。

    5. 重複チェック

    iOSの場合は過去の購入情報までまとめてレシートに入ってくるため、始めて課金をするユーザー出ない限り確実に重複が発生します。

    なので、DBに保存した過去のレシートの情報と比較して重複した場合は処理をスキップさせて行きます。

    もし1件も処理されず全件スキップされた場合は過去レシート使いまわしの不正レシートと判断してエラーを返すようにします。

    共通

    6. アイテム付与・DB保存

    やるべきことは以下のとおりです

    • アイテム付与
      • これは都度課金でも同じですね。購入した時点で付与する設定のアイテムをユーザーに付与します。
    • 月額課金特典の初期化
      • 専用ログインボーナス、専用ストアの購入制限回数などのリセット処理を行います。
      • これらのデータは別途DBにテーブルがあるはずなので、値の初期化(レコードがなければ生成)をしていきます。
    • 課金状況の保存
      • DBに月額課金の状況を保存します。開始/終了日時(datetime)・プラン加入状態(bool)などです。
      • 月額課金専用の機能のAPIなどで課金状況をチェックするために使います。

    更新時

    更新のタイミング(契約から丸々一ヶ月後の場合) 

    月額課金ということは当然ながら一定期間(大抵は1ヶ月)で更新が行われるはずです。

    そのタイミングは案件によって “月末” であったり “契約時から丸々一ヶ月後” だったりする訳ですが、私が実装した際の案件では後者だったので、それを前提に進めます。

    更新タイミングである”一ヶ月後”の算出方法は契約・更新した瞬間の該当月の日数となっています。つまり、次回更新は以下のとおりです。

    • 1ヶ月が31日ある月に契約・更新をした場合: 31日後に更新
    • 2月中に契約・更新した場合: 28日(閏年:29日)後に更新

    更新日時は、先述の通りレシートに記載されています。

    更新通知の受け取り 

    ここで問題が発生します。 更新タイミングはユーザーごとにバラバラであることです。しかも分・秒単位で。
    月末更新で一定であれば月次バッチ処理を仕込むなどして対応も可能ですが、そうはできない仕様となってしまいます。

    しかしGoogleやAppleはWebhookを用いて月額課金購入の状態をリアルタイム通知する機能を用意してくれています。

    これをサーバー側APIで受け取ってレシート検証やDB更新などをすれば、自動的に全ユーザーの更新に対応ができるようになるわけです。

    購入された時、更新された時、解約された時、決済完了した時…など、月額課金の様々なアクションが発生するたびにWebhookが飛んできますが、今回は「更新」の情報のみを取得して処理をします。

    購入時は別途レシート検証APIを先述の通り設けているので不要なのと、解約に関しては次回更新日まで課金特典の効力が続くため実装不要の仕様だったからです。

    実装フローは以下のとおりです

    1. Webhookを受け取るためのAPIを実装
    2. Webhookを飛ばすための設定
    3. 更新の準備をする

    1. Webhookを受け取るためのAPIを実装

    普段の実装はクライアントとなるスマホアプリからサーバーのAPIを叩いてもらう感じですが、今回はGoogleやAppleのサーバーからゲーム側のサーバーのAPIを叩いてもらう形式となります。なので、窓口となるAPIを実装しURLを用意してあげる必要があります。

    1. 受け取ったら、それが「更新」の通知であるかをチェックする
      • iOSの場合: リクエストにnotification_typeというstring型パラメータがあるため、その中身がDID_RENEWDID_RECOVERであるかをチェックする。
      • Androidの場合: リクエストにnotificationTypeというint型パラメータがあるため、その中身が12であることをチェックする。
      • チェック対象以外の値だった場合は更新通知ではないので処理を中断させる。
    2. 各ストアから最新のレシートを取得する
    3. レシート検証をする
      • ここまでは先述のレシート検証と同じ処理を行う。
    4. 正常だったので更新準備をする
      • 後述

    2. Webhookを飛ばすための設定

    窓口となるAPIを実装できたら、各ストア側の設定をしてWebhookが飛んでくるようにしていきます。

    こちらの説明は少々長くなるので、参考になるページへのリンクという形で割愛させてください。

    3. 更新の準備をする

    APIでレシート検証まで終わったら、ゲーム側での月額課金の更新処理を行います。

    主にやることは以下のとおりです

    • プラン終了日時の更新
      • 最新のレシートに書かれた値にする
    • アイテム付与・特典リセットの「予約」
    なぜ「予約」をするのか

    Webhookの更新通知は、大体更新日時の1〜24時間前にストア側から飛んでくる仕様となっています。時間差はだいぶアバウトですが、実際の更新日時よりも前に飛んでくることは確かです。

    つまり、更新時にアイテム付与などが本来の時間よりも前倒しに実行される懸念が発生してしまいます。

    アイテム付与だけならまだ良くて、月額課金ユーザー限定ログインボーナスのリセットが前倒しで走ってしまった場合、最悪はユーザーが最終日のアイテムを入手できずクレーム案件につながってしまいます。

    その対策として、当月分のプランが完了した後にそれらの処理を確実に実行するよう、専用のDBテーブルを用意して管理する必要があります。

    ログイン時等にそのテーブルをチェックし、本来の更新日時を過ぎていればアイテム付与・特典リセットを実行する流れです。

    月額課金のテスト

    課金のテストに実際のお金を何度も使うことは流石にせず、開発中であれば無料で課金ができる仕様で各OSは準備がしてあります。

    また、1ヶ月後の更新を動作確認でひたすら待つことが無いようにもなっており、5分経つと1ヶ月経過したとして処理されます

    更新通知もだいたい1分前に飛んでくるようになっています。

    ここで気をつけるべきことは、退会のテストを兼ねてか6ヶ月分更新した時点で自動的にプランが解約される仕様となっていることです。もちろん本番ではそのようなことは無いです。

    ただ、やはり本番運用直前までには実際のクレジットカードを使ってちゃんとテストしたほうが(精神衛生上)良いです。

    最後に

    今回は月額課金の部分のみに重きを置いたため、通常課金やそもそもの課金フローに関しては別の機会にまとめられたらいいなと思ってます。


    鳴澤さんありがとうございました!

    アピリッツでは自分が得た情報や技術を積極的に共有し、それらを吸収しながら各々のスキルアップを目指しています。
    アピリッツに少しでも興味を持った方、エントリーお待ちしております!

    記事を共有