11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Ruby(Rails)でAndroidアプリ内課金のレシートを検証するコード例

Posted at

前提

Androidアプリでの商品の購入をサーバ側でも検証する必要があった。
ので、アプリから送信されたPurchaseの情報をRubyでDeveloper APIに問い合わせ、購入を確認するコードを紹介します。
以下はRailsでの例ですが、特にRails固有の機能は使っていないので他のRuby環境でも動くはず。

検証

先に全体のコードを貼る。
検証用トークンプロダクトIDを放り込み、購入情報が返ってくれば検証成功というもの。

require 'jwt'
require 'net/http'
require 'openssl'

class ReceiptValidator
  def validate_transaction(purchase_token, product_id)
    order_id = nil
    purchase_data = nil
    # [1], [2]
    access_token = issue_request_token
    # [3]
    base = "https://www.googleapis.com/androidpublisher/v3/applications/"
    url = base +  "パッケージネーム/purchases/products/#{product_id}/tokens/#{purchase_token}"
    res_string, status_code = get_request(url, access_token)

    if status_code >= 200 && status_code < 300
      response = JSON.parse(res_string)
      # [4]
      if response && response['orderId'].present? && response['purchaseState'] == 0 # 0 => Purchased
        purchase_data = res_string
        order_id = response['orderId']
      end
      return [order_id, purchase_data]
    else
      Rails.logger.error('Could not fetch PlayStore receipt data')
      Rails.logger.error("status code: #{status_code}")
      Rails.logger.error("body: #{res_string}")
      return [nil, nil]
    end
  end

  private

  def issue_request_token # [2]
    url = 'https://oauth2.googleapis.com/token'
    header = {
      'Content-Type' => 'application/x-www-form-urlencoded'
    }
    params = {
      assertion: service_auth_jwt, # [1]で作ったJWT
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer'
    }
    body, status_code = post_request(url, header, params)
    if status_code >= 200 && status_code < 300
      res = JSON.parse(body)
      res['access_token']
    else
      Rails.logger.error('Could not fetch Google account access token')
      Rails.logger.error("status code: #{status_code}")
      Rails.logger.error("body: #{body}")
      nil
    end
  end

  def service_auth_jwt # [1]
    payload = {
      iss: Rails.application.credentials.google_service_account_client_email,
      scope: 'https://www.googleapis.com/auth/androidpublisher',
      aud: 'https://oauth2.googleapis.com/token',
      exp: Time.now.to_i + 3600,
      iat: Time.now.to_i
    }
    rsa_private = OpenSSL::PKey::RSA.new(Rails.application.credentials.google_service_account_private_key)
    JWT.encode(payload, rsa_private, 'RS256')
  end

  def get_request(url, token = nil)
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme === 'https'

    req = Net::HTTP::Get.new(uri.path)
    req['Authorization'] = "Bearer #{token}" if token
    response = http.request(req)

    [response.body, response.code.to_i]
  end

  def post_request(url, headers, params)
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme === 'https'

    req = Net::HTTP::Post.new(uri.path)
    req.set_form_data(params)
    req.initialize_http_header(headers)
    response = http.request(req)

    [response.body, response.code.to_i]
  end
end

1. JWTの作成

まず、Google Play Developer APIのサービスアカウントをJSONファイルで作成。
Roleは後からでも変更できるらしいので、権限は最低限を推奨。
https://developers.google.com/android-publisher/getting_started?hl=ja#using_a_service_account

JSONを開くとこのような構造になっているので、
https://cloud.google.com/iam/docs/creating-managing-service-account-keys

api-hogehoge.json
{
  "type": "service_account",
  "project_id": "[PROJECT-ID]",
  "private_key_id": "[KEY-ID]",
  "private_key": "-----BEGIN PRIVATE KEY-----\n[PRIVATE-KEY]\n-----END PRIVATE KEY-----\n",
  "client_email": "[SERVICE-ACCOUNT-EMAIL]",
  "client_id": "[CLIENT-ID]",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/[SERVICE-ACCOUNT-EMAIL]"
}

client_emailとprivate_keyの文字列を取り出し、JWTに以下のように投入する。
※ この例はRailsなのでRails.application.credentialsを経由している。環境に応じて投入方法を変えてください。

  def service_auth_jwt
    payload = {
      iss: Rails.application.credentials.google_service_account_client_email,
      scope: 'https://www.googleapis.com/auth/androidpublisher',
      aud: 'https://oauth2.googleapis.com/token',
      exp: Time.now.to_i + 3600,
      iat: Time.now.to_i
    }
    rsa_private = OpenSSL::PKey::RSA.new(Rails.application.credentials.google_service_account_private_key)
    JWT.encode(payload, rsa_private, 'RS256')
  end

scopeはAPIのスコープのこと。
最初なんなのかわからなかったが、使用したいAPIのリファレンスに記載されていた。
https://developers.google.com/android-publisher/api-ref/purchases/products/get

JWT自体の作成方法はこういうものとしてスルーして大丈夫です。

2. アクセストークンの取得

まだAndroid Publisher APIは叩けません。
できたJWTをhttps://oauth2.googleapis.com/tokenに放り投げ、1時間有効のアクセストークンを取得する。
※ Railsログ出力の部分Rails.logger.errorは環境によって変更もしくは削除してください。

  def issue_request_token # [2]
    url = 'https://oauth2.googleapis.com/token'
    header = {
      'Content-Type' => 'application/x-www-form-urlencoded'
    }
    params = {
      assertion: service_auth_jwt, # [1]で作ったJWT
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer'
    }
    body, status_code = post_request(url, header, params)
    if status_code >= 200 && status_code < 300
      res = JSON.parse(body)
      res['access_token']
    else
      Rails.logger.error('Could not fetch Google account access token')
      Rails.logger.error("status code: #{status_code}")
      Rails.logger.error("body: #{body}")
      nil
    end
  end

3. Android Publisher APIにリクエスト

アプリのパッケージ名、プロダクトID、アプリからPOSTされた検証用トークンをURLに埋め込み、
さらにヘッダに2で作成したアクセストークンを付与する。
※ パッケージ名は各自のものを書き込んで下さい。

    base = "https://www.googleapis.com/androidpublisher/v3/applications/"
    url = base +  "パッケージネーム/purchases/products/#{product_id}/tokens/#{purchase_token}"
    res_string, status_code = get_request(url, access_token)
    # ...略
  end

  def get_request(url, token = nil)
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme === 'https'

    req = Net::HTTP::Get.new(uri.path)
    req['Authorization'] = "Bearer #{token}" if token
    response = http.request(req)

    [response.body, response.code.to_i]
  end

4.購入情報の確認

3のリクエストに成功すると、このような検証結果が返ってくる。
https://developers.google.com/android-publisher/api-ref/purchases/products

{
  "kind": "androidpublisher#productPurchase",
  "purchaseTimeMillis": long,
  "purchaseState": integer,
  "consumptionState": integer,
  "developerPayload": string,
  "orderId": string,
  "purchaseType": integer,
  "acknowledgementState": integer
}

よい検証を行う方法はいまひとつ説明されていないが、僕は
[1] orderIdが存在する
[2] purchaseState0(Purchased)
であれば購入済みと判断するコードにしました。

使用例

アプリから検証用トークンプロダクトIDを送信、サーバ側で先程のクラスに渡す。
検証に成功したら購入情報(レシート)が返ってくるので、プロダクトIDを元にアイテムを有効にしたり、DBに記録する。

    # トークンとプロダクトIDを検証クラスに渡すと、購入情報が返ってくる
    transaction_id, purchase_data = ReceiptValidator.new.validate(receipt_data, product_id)

    if transaction_id
      # サーバ側で購入アイテムを有効にする処理、レシートを記録する処理など
    end

アプリ上のユーザアカウントとレシートを紐付けて記録しておくと、購入情報の再検証や復旧に役立ちます。
以上です。お疲れさまでした。

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?