Help us understand the problem. What is going on with this article?

iOSの月額課金レシート検証をサーバーサイドで行うときのTipsまとめ

More than 3 years have passed since last update.

最近、iOSのレシート検証をクライアントのみでの検証からサーバーサイドでの検証に実装を変更する機会があったので、その理由や方法、考慮するべき点などを書きました。参考になれば幸いです!

注意

この情報は2016年8月3日現在のものです。

レシート検証とは?

iOSで月額課金をすると、課金の証明としてAppStoreがレシートを発行します。レシートと言ってもAppStoreが紙のレシートを送りつけてくるわけではなく、電子的な購入情報のことをレシートと呼びます。ユーザーが解約処理をしない限りAppStore側でレシートが自動更新される仕組みになっています。(月額課金の場合)
その際に、AppStoreのサーバーにHTTPのPOSTリクエストでレシートを問い合わせ、現在の課金状況を知ることができます。このお問い合わせ処理と、レシートが不正なレシートでないかをチェックする処理を合わせてレシート検証と呼びます。

サーバーサイドでのレシート検証が推奨される理由

レシート検証は、クライアント側で完結させることもできます。しかし、クライアント側で完結させてしまうことで2つのデメリットが発生します。

  1. クライアント側で検証処理が閉じているためユーザーによるレシート改ざんが可能になってしまう
  2. 複数のプラットフォームで課金サービスを提供している際に、クロスデバイス対応が難しくなる

1に関して、JailBreakやroot化を使って不正レシートをサーバーサイドに送りつける例があるそうなので、サーバーサイドでのレシート検証が推奨されています。
2に関しては、機種変更やデバイスの使い分けをしている方など、iOS/Android/Web等の複数プラットフォームで同じユーザーでログインする場合があります。その場合に、サーバーサイドに検証できる仕組みがあれば別端末にログインした場合でも、より正確な課金の有無の検証が可能になります。

サーバーサイドでのレシート検証の方法

AppStoreのサーバーにHTTPのPOSTリクエストを送ることでレシートを問い合わせることができます。

リクエストURL

テスト環境用のURLと、production用のURLで分かれています。

環境 URL 用途
production https://buy.itunes.apple.com/verifyReceipt 本番用
sandbox https://sandbox.itunes.apple.com/verifyReceipt 開発時のテスト環境用
  • sandboxにはsandboxの、productionにはproduction用のレシートがあり、productionのURLにsandbox用のレシートを送るとエラーが返ってきます。
  • sandboxを利用するには、Appleのテストアカウントを取得して課金処理のテストを行います。

リクエストbody

下記の2つだけでOKです。

key サンプル
receipt-data Base64エンコードしたレシート情報 MIIjwgYJKoZIhvcNAQcCoIIjszCCI...
password アプリケーションの共有鍵 fea2ebde5...
  • receipt-dataのサンプルは省略してありますが、実際はかなり長いです。12KB程度のデータです。
  • サーバーサイドでレシートをBase64エンコードするのではなく、クライアントかAppStoreからBase64エンコード済みのデータを受け取ります。一番最初の購入時に必ずクライアントからBase64エンコード済みのレシートを送ってもらわないとサーバーサイドとAppStoreで直接レシート検証のやり取りができません。

実装上の注意

  • 審査が通るまではsandboxしか使えないのでproduction環境でもsandboxのレシートを受け付けられるような実装にしておく必要があります。
  • rubyのレシートサーバーサイド検証用のgemにveniceというgemがありますが、AppStoreのAPIのバージョンによっては動きませんでした。gemを使う場合は、ご自分のバージョンで正常に動作をするかしっかり確認してから使うことをおすすめします。

スクリーンショット 2016-07-30 16.39.15.png

[2016/08/04:追記]
veniceと別のgemを紹介していただきました。 @mono0926 さん、ありがとうございます!
https://github.com/mbaasy/itunes_receipt_validator

[2017/05/11:追記]
veniceの上記のissueは解決済みになっていました!
https://github.com/nomad/venice/issues/29

サーバーサイドレシート検証の処理構成

月額課金購入時

スクリーンショット 2016-08-03 17.50.02.png

更新時

  • 今回の実装対象のサービスは、課金処理を始めた当初、クライアント側のみで認証を行っており、途中でサーバーサイドの検証に変更したため、古くから課金しているユーザーはサーバーサイドにBase64エンコード済みのレシートを保持していません。なので、一度クライアント側に問い合わせてレシートを受け取ってから検証を行うようにしました。
  • アプリ立ち上げ時の課金状況の問い合わせを毎回行うと重くなるため、ユーザーテーブルに保持してある課金期限が過ぎているユーザーのみ行っています。

スクリーンショット 2016-08-03 17.50.10.png

AppStoreから返ってくるレシート情報の項目

APIが返す項目の公式ドキュメントは下記のリンクです。

公式ドキュメント

注意しなければならないのが、古くから課金コンテンツを営んでいるアプリだとその当時のバージョンのAPIが返ってきます。今回の実装対象のサービスの課金コンテンツは2014年に開始されているためか、iOS6タイプのAPIが返ってきています。

その証拠に、only returned for iOS6 と書いてある下記の2つの項目がAPIのレスポンスに含まれています。

スクリーンショット 2016-07-31 17.40.48.png

途中でサーバーサイド検証に置き換えるような場合、最新ドキュメントの通りにAPIが返らない可能性があるため注意が必要です。ドキュメントだけを信頼せず、実際に返ってくるフィールドを見ながら実装をすすめることをおすすめします。(iOS7以降のAPIバージョンでもiOS6のみが返す項目と書いてあるlatest_receiptが返るという記事もあったので、公式ドキュメントの全てが正しい情報ではないかもしれません。)

※ [危険!!!!]iOS6タイプのトランザクション形式を使っている人への注意
receiptフィールドに含まれるin_appという項目は、公式ドキュメントの説明文には

In the JSON file, the value of this key is an array containing all in-app purchase receipts. 

と書いてありますが、実際には1ヶ月毎の更新が少し遅れることがあったので最新レシートの認証を行う場合はlatest_receipt_infoのほうを使うことをおすすめします。こちらのほうはリアルタイムで更新されます。

[2016/08/04:追記]
Use in_app or latest_receipt_info for getting latest receipt for auto-renewable iOS 7 style transactions?

この記事とまさに同じことが起こりました

The "in_app" JSON element contains LESS receipts than "latest_receipt_info". I was expecting both
elements to contain the same number or receipts.

Also, I was expecting that the "in_app" element would contain ALL the receipts. However, it appears
that "latest_receipt_info" actually contains all the receipts. Apple documentation seems to suggest
to use "in_app" for finding a latest receipt.

検証内容

レシートの内容で検証すべき項目

項目 項目の内容 検証内容
status 0であれば正常なレシート、その他は不正なレシート(エラーコード表参照) AppStoreから正常なレシートが返ってきているか
in_app又はlatest_receipt_info 過去の購入履歴 課金履歴が存在しているか
bundle_id iTunesConnectで設定したCFBundleIdentifierの値 自分のアプリのものか
product_id iTunesConnectで設定したproductIdentifierの値 意図した商品への課金か
transaction_id 1ヶ月ごとのレシート毎に発行される固有のid 別のユーザーのレシートを使っていないか
original_transaction_id ProductごとのAppStore固有のid 同じAppStoreで別のアカウントが既に課金していないか
expires_date レシートの期限 期限は切れていないか

各テーブルに保持すべき項目

ユーザーテーブルに持つべき項目

項目 内容 用途
purchase_device どのプラットフォームで課金しているユーザーなのか クロスデバイス対応の際に有用
premium_expire_time 課金の期限 レシート検証対象のユーザーかどうかをチェック

レシートテーブルに持つべき項目

user_idとレシートの内容全部

サーバーサイドのレシート検証で考慮すべきこと

クロスデバイス

状況

  • AndroidからiOSへの機種変更時(逆も然り)
  • 複数デバイスでのログイン

対処方法

  • ユーザーテーブルに保持したpurchase_deviceを確認し、どのプラットフォームで課金しているユーザーなのかをチェック
  • AppStoreのレシート検証が通らなかったら、別のデバイスの検証で課金の有無をチェック

例)
スクリーンショット 2016-08-02 17.29.41.png

ダブルアカウント

状況(ニッチな状況ですが)

  • アカウントを2つ持っている
  • 過去に片方のアカウントで課金していたことがある
  • 現在つかっているAppStoreのアカウントが過去に課金していたAppStoreと同じアカウント
  • original_transaction_idによる認証をサーバーサイドで行っている(original_transaction_idはProductごとにAppStoreで固有の値なので、前に課金していた時と再度課金する時のどちらも同じ値で返ります)
  • もう片方のアカウントでもう一度課金したい

対処方法

  • original_transaction_idが一緒な他のユーザーが居ても、片方のアカウントでユーザーテーブルのexpires_dateが過ぎていれば認証OKにする

例)
スクリーンショット 2016-08-02 17.29.47.png

まとめ

iOSのサーバーサイドレシート検証の実装のメリットから実際の検証方法、考慮する点をまとめました。
状況によって考慮すべき点や検証すべき項目、ハマるポイントが違うと思うので、公式ドキュメントはもちろんのこと、複数の実装事例の記事を読んでみると良いと思います。
最後まで読んでいただき、ありがとうございました!

参考資料

  • 公式ドキュメント

Validating Receipts With the App Store
レシート検証プログラミングガイド

  • ブログ

参考になったブログです。ありがとうございます!

自動購読課金について【iOS編】
iOS/Androidアプリ内課金の不正なレシートによる有料会員登録を防ぐ

dely
世界をより明るくするために、「kurashiru」を開発している
https://www.dely.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away