iTunesアプリ内購入サーバー側

iTunesを介した支払いは、モバイルアプリケーションによって提供されるコンテンツの収益化における実際のリーダーです。 私が知っているアプリケーションの1つでは、それらからの収入はGoogle Playユーザーからの収入の3倍であり、後者のトラフィックは1.5倍です。 したがって、単一のGoogle Playユーザーよりも単一のiTunesユーザーから最大5倍の金額を受け取ることができます。 iTunesの支払いをモバイルアプリケーションに統合するには、この議論で十分です。



この記事では、サーバー側からのiTunes支払い(サブスクリプションを含む)の検証のいくつかの機能について説明します。







開発者のガイドに従って、2つの支払いトランザクション検証スキームが提案されています。シンプル、トランザクション確認はモバイルアプリケーションとApp Store間の対話の結果であり、複雑です。 2番目のケースでは、iTunes Connectサービスにアクセスすることにより、独自のサーバーから追加の検証手順が導入されます。 iTunes Connectを介した支払い取引の成功確認は、支払いを確認するのに十分であると見なされます。

単純な検証の欠点には、 信頼の浸食が含まれます 。 複雑な利点には、サブスクリプションでの作業の利便性、世俗的な商品を請求し、サーバー側で製品のリストを保存する機能が含まれます。 最後の2つのポイントは、App Storeでアプリケーションの更新を1週間待つ必要がある場合に特に関連します。 または、不適格なクリスマスの前夜に魅力的な製品でユーザーを喜ばせることに突然決めた場合は、おそらく数週間です。 私はセキュリティについても話していません-次のチャートではすべてが明確です:







したがって、抽象的なアプリケーションの支払い要求を監視するシステムでは、非常に普通の日に見えるかもしれません。 青色は、支払い確認リクエストの総数を表します。 緑-実際にApp Storeを通過したリクエスト。 赤-悪意のあるリクエスト。 サーバーが支払いの確認を無視した場合、アプリケーションがどのような利益の損失を得るか想像するのは怖いです。 次の表に、グラフのデータの割合を示します。



リクエスト機能 割合
未確認。 正しいものに似たデータで構成される偽の支払い。ただし、フィールドが存在しないか、番号が線として表示されるか、支払いを確認できない他の特徴的な機能があります。 0.7%
繰り返し。 支払いが確認されたが、しばらくしてから再度送信されたクライアントからのリクエスト 1%
クラッカーの支払い( iAP Crackerなど)。 彼らは自分で確認のために作成された検証の支払いを送信します。 9.3%
偽物。 他のアプリケーションのためにiTunesを介して確認された支払い 79%
確認済み。 本当に公正な買い物。 その数は、アカウントを介した購入数と収束します 10%




実際、ほとんどの悪意のあるリクエストは、検証サービスへのアクセスにトラフィックを費やすことなく、独自に判断できます。 支払いiTunesは、いわゆる レシピ 。 レシピは、支払い取引データ用のbase64でエンコードされたJSONオブジェクトです。 App Storeサービスを介した支払いまたはサブスクリプションを確認するには、クライアントアプリケーションによって報告されたレシピを転送する必要があります。 それに応じて、処方箋のステータスと支払いの詳細を受け取ります。



正しいレシピを検討してください(以降、正しいレシピのデータはわずかに変更されます)。



$ php -r "var_dump(base64_decode('Re4LRece1PT='));" string(2453) "{ "signature" = "8iN4rY5iGNaTUrE=="; "purchase-info" = "PuRCh45e1nf0RM4tIoN=="; "pod" = "22"; "signing-status" = "0"; }" $ php -r "var_dump(base64_decode('PuRCh45e1nf0RM4tIoN=='));" string(784) "{ "original-purchase-date-pst" = "2013-02-18 10:05:51 America/Los_Angeles"; "purchase-date-ms" = "1361210751012"; "unique-identifier" = "aun1que1dent1f1er"; "original-transaction-id" = "1234567890"; "bvrs" = "220"; "app-item-id" = "123"; "transaction-id" = "1234567890"; "quantity" = "1"; "original-purchase-date-ms" = "1361210751012"; "unique-vendor-identifier" = "VEND0R-1DENT1F1ER"; "item-id" = "456"; "version-external-identifier" = "789"; "product-id" = "com.example.application.product.1"; "purchase-date" = "2013-02-18 18:05:51 Etc/GMT"; "original-purchase-date" = "2013-02-18 18:05:51 Etc/GMT"; "bid" = "com.example.application"; "purchase-date-pst" = "2013-02-18 10:05:51 America/Los_Angeles"; }"
      
      







レシピは、購入データ、署名、およびサービスフィールドのペアで構成されます。 署名はバイナリで、base64でエンコードされています。 購入データもエンコードされ、多くのフィールドを持つJSONオブジェクトです。 2つのフィールドが最も興味深いと考えています。product-id-購入した製品の識別子とbid-アプリケーションの識別子です。



悪意のあるクエリセレクター-偽のクエリ-このようなもの:



 $ php -r "var_dump(base64_decode('CHuZH0iRECE1pt=='));" string(2281) "{ "signature" = "8iN4rY5iGNaTUrE=="; "purchase-info" = "4n0THeRPuRCh45e1nf0RM4tIoN=="; "pod" = "17"; "signing-status" = "0"; }" $ php -r "var_dump(base64_decode('4n0THeRPuRCh45e1nf0RM4tIoN=='));" string(656) "{ "original-purchase-date-pst" = "2012-07-12 05:54:35 America/Los_Angeles"; "purchase-date-ms" = "1342097675882"; "original-transaction-id" = "170000029449420"; "bvrs" = "1.4"; "app-item-id" = "450542233"; "transaction-id" = "170000029449420"; "quantity" = "1"; "original-purchase-date-ms" = "1342097675882"; "item-id" = "534185042"; "version-external-identifier" = "9051236"; "product-id" = "com.zeptolab.ctrbonus.superpower1"; "purchase-date" = "2012-07-12 12:54:35 Etc/GMT"; "original-purchase-date" = "2012-07-12 12:54:35 Etc/GMT"; "bid" = "com.zeptolab.ctrexperiments"; "purchase-date-pst" = "2012-07-12 05:54:35 America/Los_Angeles"; }"
      
      







かなりまともなレシピ。 アプリケーションからではありません。 iTunes Connectに連絡すると、この支払いの確認が届きます。



 $ wget 'https://buy.itunes.apple.com/verifyReceipt' -q --post-data='{"receipt-data":"CHuZH0iRECE1pt=="}' -O - {"receipt":{"original_purchase_date_pst":"2012-07-12 05:54:35 America/Los_Angeles", "purchase_date_ms":"1342097675882", "original_transaction_id":"170000029449420", "original_purchase_date_ms":"1342097675882", "app_item_id":"450542233", "transaction_id":"170000029449420", "quantity":"1", "bvrs":"1.4", "version_external_identifier":"9051236", "bid":"com.zeptolab.ctrexperiments", "product_id":"com.zeptolab.ctrbonus.superpower1", "purchase_date":"2012-07-12 12:54:35 Etc/GMT", "purchase_date_pst":"2012-07-12 05:54:35 America/Los_Angeles", "original_purchase_d
      
      







原則として、彼らはチェックできませんでした。 クライアントアプリケーションからレシピを受け取った段階でも、 product-idbidをアプリケーションの許容可能なものと比較することで、iTunesへのトラフィックの80%を節約できます。



クラッカーによって作成されるレシピは非常に原始的です: Y29tLnVydXMuaWFwLjk2NjU3Mjkw



。 復号化、 com.urus.iap.96657290



を取得しcom.urus.iap.96657290



。 明らかに、ここではレシピの構造についても言及していません-署名も購入データもありません。 このようなレシピは安全に拒否できます。 このようなレシピのiTunesはエラー21002を返します。



クラッカーによって作成されたレシピを受け取った場合、自分でレシピを検証することも、iTunesサービスを使用してレシピを検証することもできれば、重複は自分の側でしか見つかりません。 利益が発生したすべてのトランザクション識別子を保存し、過去の識別子の中にそのような識別子がすでにあるかどうかをチェックするだけで十分です。 ドキュメントによるとすべてのトランザクション識別子は支払いを一意に識別します。



サンプル内の最小の悪は、未確認のレシピです。 以下に例を示します。

 $ php -r "var_dump(base64_decode('P0dDe1NyRECE1pt=='));" string(613) "{"signing-status"="0";"purchase-info"="P0dDe1N0e1NF0==";"pid"="143";"signature"="1POdP1sD4jEe5t=";}" $ php -r "var_dump(base64_decode('P0dDe1N0e1NF0=='));" string(388) "{"unique-identifier"="an0theru1que1dent1f1er";"purchase-date"="2012-02-18 19:23:27 Etc/GMT";"original-transaction-id"="0123456789";"quantity"="1";"original-purchase-date"="2012-02-18 19:23:27 Etc/GMT";"bvrs"="123";"product-id"="com.example.application.product.1";"item-id"="456";"transaction-id"="0123456789";"bid"="com.example.application";}"
      
      







この場合、適切なレシピと比較して、スペースを節約しますが、これはレシピを拒否する理由ではありません-結局、それはクライアントアプリケーションによってコンパイルおよびエンコードされます。 そして、レシピは正しいように見えます。正しい製品とアプリケーションの識別子、もっともらしい支払いデータ、署名があります。 iTunesにリクエストを送信する必要があります(このようなリクエストは、総数のわずか0.7%、有用なリクエストの数の7%であることが望ましいです)。 iTunesはコード21002で応答します。



次の図は、独自のサーバー側のクライアントアプリケーションから受信したレシピを検証するアルゴリズムを示しています。











iTunesで直接確認するには、小さくて便利なライブラリを使用することをお勧めします 。 このライブラリを使用すると、 更新されたサブスクリプションも確認できます。 同じ検証要求を使用して、クライアントの初期化時にシークレットパスワードを指定して、サブスクリプション情報を要求できます。



 $AppStore = new \AppStore\Client\AppStoreClient(); $AppStore->setPassword('secret shared password') ->setSandbox((bool) mt_rand(0,1)); $Status = $AppStore->verifyReceipt('5t4TUs==');
      
      







iTunesは次の形式で応答データを返します



 object(AppStore\Client\Response\RenewableStatus)#7 (4) { ["latestReceipt":"AppStore\Client\Response\RenewableStatus":private]=> string(3460) "5t4TUs==" ["LatestReceiptInfo":"AppStore\Client\Response\RenewableStatus":private]=> object(AppStore\Client\Response\RenewableReceipt)#8 (11) { ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=> string(13) "1363547483000" ["quantity":"AppStore\Client\Response\Receipt":private]=> int(1) ["productId":"AppStore\Client\Response\Receipt":private]=> string(35) "com.example.application.product.2" ["transactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "0987654321" ["purchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-02-18 20:11:23 Etc/GMT" ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "9078563412" ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-01-18 20:11:25 Etc/GMT" ["appItemId":"AppStore\Client\Response\Receipt":private]=> string(9) "456" ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=> string(0) "" ["bid":"AppStore\Client\Response\Receipt":private]=> string(19) "com.example.application" ["bvrs":"AppStore\Client\Response\Receipt":private]=> string(3) "123" } ["status":"AppStore\Client\Response\Status":private]=> int(0) ["Receipt":"AppStore\Client\Response\Status":private]=> object(AppStore\Client\Response\RenewableReceipt)#9 (11) { ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=> string(13) "1363547483000" ["quantity":"AppStore\Client\Response\Receipt":private]=> int(1) ["productId":"AppStore\Client\Response\Receipt":private]=> string(35) "com.example.application.product.2" ["transactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "0987654321" ["purchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-02-18 20:11:23 Etc/GMT" ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "9078563412" ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-01-18 20:11:25 Etc/GMT" ["appItemId":"AppStore\Client\Response\Receipt":private]=> string(9) "456" ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=> string(0) "" ["bid":"AppStore\Client\Response\Receipt":private]=> string(19) "com.example.application" ["bvrs":"AppStore\Client\Response\Receipt":private]=> string(3) "123" } }
      
      







Google Playサブスクリプションとは異なり、iTunesは支払い期間ごとに新しいサブスクリプションレシピを作成します。 次の請求期間の開始の約1日前に、iTunesはユーザーのアカウントからお金を引き出そうとしますが、新しい請求期間の開始の48時間前に更新料金が引き落とされるという苦情を見ました。 試行がまだ実行されておらず、まだ成功していない場合、上記の例のように、 latest_receiptに表示されるデータは元のレシピのデータと一致します。 サブスクリプションが正常に更新されると、自動購入のデータがlatest_receipt_infoフィールドに表示され、エンコードされたレシピがlatest_receiptフィールドに表示されます



 object(AppStore\Client\Response\RenewableStatus)#7 (4) { ["latestReceipt":"AppStore\Client\Response\RenewableStatus":private]=> string(3460) "ReNEW481E5t4TUs==" ["LatestReceiptInfo":"AppStore\Client\Response\RenewableStatus":private]=> object(AppStore\Client\Response\RenewableReceipt)#8 (11) { ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=> string(13) "1363547483000" ["quantity":"AppStore\Client\Response\Receipt":private]=> int(1) ["productId":"AppStore\Client\Response\Receipt":private]=> string(35) "com.example.application.product.2" ["transactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "0987654321" ["purchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-02-18 20:11:23 Etc/GMT" ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "9078563412" ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-01-18 20:11:25 Etc/GMT" ["appItemId":"AppStore\Client\Response\Receipt":private]=> string(9) "456" ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=> string(0) "" ["bid":"AppStore\Client\Response\Receipt":private]=> string(19) "com.example.application" ["bvrs":"AppStore\Client\Response\Receipt":private]=> string(3) "123" } ["status":"AppStore\Client\Response\Status":private]=> int(0) ["Receipt":"AppStore\Client\Response\Status":private]=> object(AppStore\Client\Response\RenewableReceipt)#9 (11) { ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=> string(13) "1361131883894" ["quantity":"AppStore\Client\Response\Receipt":private]=> int(1) ["productId":"AppStore\Client\Response\Receipt":private]=> string(35) "com.example.application.product.2" ["transactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "0987654312" ["purchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-01-18 20:11:23 Etc/GMT" ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "9078563412" ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-01-18 20:11:25 Etc/GMT" ["appItemId":"AppStore\Client\Response\Receipt":private]=> string(9) "456" ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=> string(0) "" ["bid":"AppStore\Client\Response\Receipt":private]=> string(19) "com.example.application" ["bvrs":"AppStore\Client\Response\Receipt":private]=> string(3) "123" } }
      
      







サブスクリプションを更新できなかった場合、応答ステータスが返されます21006



 $AppStore = new \AppStore\Client\AppStoreClient(); $AppStore->setPassword('secret shared password') ->setSandbox((bool) mt_rand(0,1)); try { $Status = $AppStore->verifyReceipt('ExP1ReD5t4TUs=='); } catch (\AppStore\Client\Response\ExpiredSubscriptionException $ex) { var_dump($ex->getStatus()); }
      
      







 object(AppStore\Client\Response\RenewableStatus)#7 (4) { ["latestReceipt":"AppStore\Client\Response\RenewableStatus":private]=> string(0) "" ["LatestReceiptInfo":"AppStore\Client\Response\RenewableStatus":private]=> NULL ["status":"AppStore\Client\Response\Status":private]=> int(21006) ["Receipt":"AppStore\Client\Response\Status":private]=> object(AppStore\Client\Response\RenewableReceipt)#8 (11) { ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=> string(13) "1361208738953" ["quantity":"AppStore\Client\Response\Receipt":private]=> int(1) ["productId":"AppStore\Client\Response\Receipt":private]=> string(35) "com.example.application.product.2" ["transactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "2143658709" ["purchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-01-18 17:32:18 Etc/GMT" ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=> string(15) "2143658709" ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=> string(27) "2013-01-18 17:32:19 Etc/GMT" ["appItemId":"AppStore\Client\Response\Receipt":private]=> string(9) "456" ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=> string(0) "" ["bid":"AppStore\Client\Response\Receipt":private]=> string(19) "com.example.application" ["bvrs":"AppStore\Client\Response\Receipt":private]=> string(3) "123" } }
      
      







次のサーバー側のiTunesサブスクリプション処理スキームを提案します。











説明:







私のデータによると、iTunesサブスクリプションの約60%が更新されています。 Google Playサブスクリプションの場合、この値は約40%です。 そして、サブスクリプションの更新が不可能なケースの大部分は、ユーザーアカウントの資金不足のケースです。



All Articles