自動購読課金について【Android編】 | サイバーエージェント 公式エンジニアブログ

はじめに

AWAサーバサイドエンジニアの辻(jun06t)です。

前回の続きで今回はAndroidの月額課金のための実装について書かせていただきます。
基本的な流れは前回と同じになってます。

注意事項

※1:開発中にプラットフォーム側の仕様変更があったため、記載している内容は情報が古い可能性があります。
※2:記載している動作は十分に調査できていないものも含んでいるため、内容が不正確である可能性があることをご了承ください。

対象環境

目次

  1. Google公式ドキュメント
  2. Androidでの購読登録処理の流れ
  3. 署名の検証
  4. データフォーマット
  5. レシートの検証
  6. 購読期間
  7. 自動更新手順
  8. アップグレードとダウングレード
  9. 実装していて困ったこと
  10. レシート検証用ライブラリの紹介
  11. まとめ
  12. 謝辞

Google公式ドキュメント

Androidでの購読登録処理の流れ

Androidでの流れです。AndroidはGoogleが署名を発行してくれるのでiOSよりもセキュアに検証できます。

android_flow

レシートと書いてありますが、実際は後述するsubscriptionId,tokenというフィールド名で扱われます。
また署名検証をする場合は決済時の全フィールドをサーバ(上図ではAWAサーバ)へ送信してください。

署名の検証

署名を検証することで、クライアントから送信されるレシートが改竄されていないか確認することができます。

SHA1でハッシュ化された署名なので、golangの場合以下のように検証します。
署名の復号に使う公開鍵はGooglePlayDeveloperConsoleで確認できます。

const (
        base64EncodedPublicKey = "--- your app's public key ---"
)

func verify(receipt interface{}, signature string) (bool, error) {
        // prepare public key
        decodedPublicKey, err := base64.StdEncoding.DecodeString(base64EncodedPublicKey)
        if err != nil {
                return false, fmt.Errorf("failed to decode public key")
        }
        publicKeyInterface, err := x509.ParsePKIXPublicKey(decodedPublicKey)
        if err != nil {
                return false, fmt.Errorf("failed to parse public key")
        }
        publicKey, _ := publicKeyInterface.(*rsa.PublicKey)

        // decode signature
        decodedSignature, err := base64.StdEncoding.DecodeString(signature)
        if err != nil {
                return false, fmt.Errorf("failed to decode signature")
        }

        // generate hash value from receipt
        b, err := json.Marshal(receipt)
        if err != nil {
                return false, fmt.Errorf("failed to JSON marshal")
        }
        hasher := sha1.New()
        hasher.Write(b)
        hashedReceipt := hasher.Sum(nil)

        // verify
        if err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA1, hashedReceipt, decodedSignature); err != nil {
                return false, nil
        }

        return true, nil
}

署名検証するレシートのフィールドは以下の通りです。

Androidのデータフォーマット

次はサーバ - GooglePlayAPI間の検証処理について述べていきます。
GooglePlayAPIへは以下のリクエストを投げます。

RequestURL

RequestParameters

ResponseBody

レスポンスは以下の様な情報が返ります。

{
  "kind": "androidpublisher#subscriptionPurchase",
  "startTimeMillis": 1437567760835,
  "expiryTimeMillis": 1445631222776,
  "autoRenewing": true
}

また失敗時は以下の様なエラーが返ります。

エラーステータス

レシートの検証

iOSと違って前述の署名検証とGoogleへのリクエストが成功していれば正しいレシートです。

ただし別ユーザが不正にその正規レシートを送信した可能性を考慮して、レシートのdeveloperPayloaduserIDを埋め込み、リクエストを送信したユーザのuserIDと一致するかを検証する必要があります。

Androidの購読期間

以下の4種類あります。

  • 1週間
  • 1ヶ月
  • 1年
  • 季節ごと(季節の終わりで停止し、来年の季節が始まると再開)

Androidの自動更新手順

購読時に発行されたtokenはキャンセルするまで変わりません。したがって毎回クライアントからレシートを送る必要はありません。サーバだけで検証できます。

  1. 期限日にGoogle Playが購読の自動更新を開始
  2. 保持しているpurchaseTokenを用い、サーバからGoogleの検証APIを叩く
  3. レスポンスのexpiryTimeMillisを見て購読期限日を更新

iOSと異なり最初に送信されたレシートを使えばいつでも購読状態を確認できるので、クライアントでは特に処理は入れずサーバで1日1回程度チェックすれば良いでしょう。

アップグレードとダウングレード

AndroidではGooglePlayがアップグレード、ダウングレードの機能を提供しています。変更後は変更前の購読状態が自動でキャンセルになります。ですのでこの機能を用いればiOSと異なりユーザが二重購読する状態にはなりません。

ただし実装する上で、以下のように請求金額および購読期間の変化があるため注意が必要です。

Androidで実装していて困ったこと

期限切れの検証期間が長い

テストアカウントだと24時間で期間が切れます。
iOSのSandbox環境と違って短くはならないため、翌日になるまで待つ必要がありました。

アップグレード、ダウングレードがドキュメントにあるが、Androidのライブラリにない

これはどうしようもなかったためAWAのAndroidエンジニアである沖本さんが自作しました。以下のStack Overflowでgistリンクを貼っています。

How to upgrade/downgrade subscriptions in Android InAppBilling?

テストアカウントではダウングレードしても期間が伸びない

先に説明したように、ダウングレード時は先に余分に支払ったお金の分、購読期間が延長されます。
しかしテストアカウントでのダウングレードでは期間は延長されませんでした。

購読キャンセルをしても、autoRenewingの状態がすぐには変わらない

GooglePlayAPIのレスポンスにはautoRenewingという自動購読状態かどうかのフラグがあります。
AndroidのPlayStoreなどからキャンセルした後はこのフラグがfalseに変わりますが、15~30分ほど経つまで変わりませんでした。
ユーザとしてはキャンセルしたはずなのに、GooglePlayAPIから返る値は未だキャンセルされていない、とみなされるため、アプリ内でキャンセル済みか表示する場合は定期的にautoRenewingの状態をチェックする必要があります。

orderIdは保持しておこう

レシートの検証ではorderIdは特に使用しません。しかしながらGooglePlayの課金レポートではorderIdベースで課金情報が登録されるため、ユーザサポートなどで必要になったりします。

レシート検証用ライブラリの紹介

AWAのサーバはgolangで実装されており、golangでのレシート検証用ライブラリが古いものしかなかったためこちらも自作しました。AppStoreAPI、GooglePlayAPIの両方に対応しています。

dogenzaka/go-iap

正規レシートを公開できないためテストコードは不足していますが、現在AWAで使用していますので動作自体は問題ないと思います。プルリク歓迎してます。

まとめ

手探りしながら導入した自動購読の課金処理ですが、色んな問題に直面した分きちんとした実装ができたと思っています。
今後ネイティブアプリで自動購読を導入する方にとって少しでも参考になれば幸いです。

謝辞

AWAでのAndroid課金実装、そしてこの記事を書くにあたって、Androidエンジニアの沖本さん(cre8ivejp)には非常にお世話になりました。本当にありがとうございます。