前提
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
{
"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] purchaseState
が0
(Purchased)
であれば購入済みと判断するコードにしました。
使用例
アプリから検証用トークンとプロダクトIDを送信、サーバ側で先程のクラスに渡す。
検証に成功したら購入情報(レシート)が返ってくるので、プロダクトIDを元にアイテムを有効にしたり、DBに記録する。
# トークンとプロダクトIDを検証クラスに渡すと、購入情報が返ってくる
transaction_id, purchase_data = ReceiptValidator.new.validate(receipt_data, product_id)
if transaction_id
# サーバ側で購入アイテムを有効にする処理、レシートを記録する処理など
end
アプリ上のユーザアカウントとレシートを紐付けて記録しておくと、購入情報の再検証や復旧に役立ちます。
以上です。お疲れさまでした。