App Store 服务器 API 实战
前言:先说IAP的事,原本我们的支付功能需要S2S验证收据的真实性。但是以前的收据[appStoreReceiptURL]在>StoreKit2中无法获取。
查阅了现在收据原来是在JWS中,可是发现这个JWS我们后台验证还没开始实现(#_#)。
没办法,一起找啊找,各种关键词: StoreKit2 verify AppleDeveloper Forums...丢人就不念了。
直到遇见了这篇文章 转载一下,希望也可以解决你现在遇到的问题。
正文开始
App Store Server API – 是一种新的 REST API,可让您获取有关所有客户应用内购买的信息。与旧的 verifyReceipt 端点的主要区别在于,您不再需要向服务器发送大型 base64 收据。
检索信息是使用原始事务 ID 完成的,请求和响应都使用 JWT 和从 App Store Connect 生成的 API 密钥进行签名。
生成应用内购买 API 密钥
为应用内购买生成密钥与生成订阅密钥相同——该选项卡已被简单地重命名:【PS: 如果看不到这个选项,说明你的权限不够高】
要为应用内购买生成 API 密钥,请访问:
用户和访问
钥匙
在应用程序内购买
下载密钥并将其保存到安全的地方。请注意,您只能下载一次密钥。
发行人编号
要创建请求,您还需要颁发者 ID,可以在Keys > App Store Connect API选项卡中找到它。如果页面上缺少此字段,您可能需要创建您的第一个 App Store Connect API 密钥,即使您不会使用它。您也可以尝试从所有者帐户签名。
创建 JWT
JSON Web Token (JWT) 使用开放标准RFC 7519,该标准定义了一种安全传输信息的方式。
生成令牌使用 3 个步骤完成:
创建 JWT 标头
创建 JWT 负载
签署 JWT
Header 包含三个字段:
{ "alg": "ES256", "kid": "2X9R4HXF34", "typ": "JWT" } 复制代码
其中alg
和typ
– 静态值,以及kid
– 是您的密钥 ID。
JWT 负载如下所示:
{ "iss": "57246542-96fe-1a63e053-0824d011072a", "iat": 1623085200, "exp": 1623086400, "aud": "appstoreconnect-v1", "nonce": "nonce6-12b482e82" 0242ac130003" , “中标”: “com.apphud” } 复制代码
iss
– 是我们从 App Store Connect 获得的 Issuer ID。
iat
– 令牌创建日期,以秒为单位。
exp
– 令牌到期日期,以秒为单位。必须在令牌创建日期之后不到 1 小时。
aud
– 静态值“appstoreconnect-v1”。
nonce
– 一个随机的唯一请求标识符,“salt”。
bid
– 应用程序的捆绑 ID。
可以在此处找到有关 JWT 有效负载的更多信息。
获取交易信息
要获取交易列表,您需要订阅的原始交易 ID。默认情况下,API 一次返回 20 个事务,从旧到新排序。如果有超过 20 笔交易,则参数hasMore
将为true
。
网址如下:
https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{original_transaction_id} 复制代码
在沙盒域中是以下内容:
https://api.storekit-sandbox.itunes.apple.com 复制代码
JWT 库非常流行,适用于所有主要语言。我们将在Ruby
.
让我们创建StoreKit
类:
require 'jwt' require_relative 'jwt_helper' require 'httparty' class StoreKit ... attr_reader :private_key, :issuer_id, :original_transaction_id, :key_id, :bundle_id, :response ALGORITHM = 'ES256' def jwt JWT.encode( payload, private_key, ALGORITHM, headers ) end def headers { kid: key_id, typ: 'JWT' } end def payload { iss: issuer_id, iat: timestamp, exp: timestamp(1800), aud: 'appstoreconnect-v1', nonce: SecureRandom.uuid, bid: bundle_id } end end 复制代码
这里很简单。我们刚刚定义了我们之前描述的方法。
现在让我们添加 URL 变量并添加一些代码来启动请求:
URL = 'https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions/%<original_transaction_id>s' def request! url = format(URL, original_transaction_id: original_transaction_id) result = HTTP.get(url, headers: { 'Authorization' => "Bearer #{jwt}" }) # raise UnauthenticatedError if result.code == 401 # raise ForbiddenError if result.code == 403 result.parsed_response end 复制代码
要调用此代码,让我们创建一个单独的文件subscription.rb
,在其中初始化我们的StoreKit
类实例并调用它:
key_id = File.basename(ENV['KEY'], File.extname(ENV['KEY'])).split('_').last ENV['KEY_ID'] = key_id StoreKit.new( private_key: File.read("#{Dir.pwd}/keys/#{ENV['KEY']}"), issuer_id: '69a6de82-48b4-47e3-e053-5b8c7c11a4d1', original_transaction_id: ENV['OTI'], key_id: key_id, bundle_id: 'com.apphud' ).call 复制代码
在响应中,我们得到带有 JWT 签名字段的 JSON。
解码响应
要解码响应,我们需要一个公钥。它可以从我们的私钥中提取。让我们写一个辅助类JWTHelper
:
require 'jwt' require 'byebug' require 'openssl/x509/spki' # JWT class class JWTHelper ALGORITHM = 'ES256' def self.decode(token) JWT.decode(token, key, false, algorithm: ALGORITHM).first end def self.key OpenSSL::PKey.read(File.read(File.join(Dir.pwd, 'keys', ENV['KEY']))).to_spki.to_key end end 复制代码
此类使用 OpenSSL 库读取私钥并使用to_spki
方法(简单公钥基础结构)提取公钥。然后使用公钥和 ES256 算法从响应中解码 JWT。
让我们解码我们的响应:
def decoded_response response['data'].each do |item| item['lastTransactions'].each do |t| t['signedTransactionInfo'] = JWTHelper.decode(t['signedTransactionInfo']) t['signedRenewalInfo'] = JWTHelper.decode(t['signedRenewalInfo']) end end response end 复制代码
如果一切正常,我们将得到最终的 JSON:
{ "environment": "Sandbox", "bundleId": "com.apphud", "data": [ { "subscriptionGroupIdentifier": "20771176", "lastTransactions": [ { "originalTransactionId": "1000000809414960", "status": 2, "signedTransactionInfo": { "transactionId": "1000000811162893", "originalTransactionId": "1000000809414960", "webOrderLineItemId": "1000000062388288", "bundleId": "com.apphud", "productId": "com.apphud.monthly", "subscriptionGroupIdentifier": "20771176", "purchaseDate": 1620741004000, "originalPurchaseDate": 1620311199000, "expiresDate": 1620741304000, "quantity": 1, "type": "Auto-Renewable Subscription", "inAppOwnershipType": "PURCHASED", "signedDate": 1623773050102 }, "signedRenewalInfo": { "expirationIntent": 1, "originalTransactionId": "1000000809414960", "autoRenewProductId": "com.apphud.monthly", "productId": "com.apphud.monthly", "autoRenewStatus": 0, "isInBillingRetryPeriod": false, "signedDate": 1623773050102 } } ] } ] } 复制代码
如您所见,lastTransactions
数组包含有关订阅的最后一笔交易以及订阅状态的信息。状态字段的值为2
,这意味着expired
。此处描述了所有订阅状态。
还有一个新字段"type": "Auto-Renewable Subscription"
,它是人类可读字符串中的应用内购买类型。
不幸的是,新 API 中仍然缺少交易价格。【以下是我贴的以前的旧收据,本来想单独写一篇文章的,想想没必要】
//iOS14.0 收据解析结果 { "environment" : "Sandbox", "receipt" : { "adam_id" : 0, "app_item_id" : 0, "application_version" : "1", "bundle_id" : "com.xxx.xxx.ios", "download_id" : 0, "in_app" : [ { "in_app_ownership_type" : "PURCHASED", "is_trial_period" : "false", "original_purchase_date" : "2021-11-19 07:57:08 Etc\/GMT", "original_purchase_date_ms" : "1637308628000", "original_purchase_date_pst" : "2021-11-18 23:57:08 America\/Los_Angeles", "original_transaction_id" : "1000000914137632", "product_id" : "xx.xx.xx.xx", "purchase_date" : "2021-11-19 07:57:08 Etc\/GMT", "purchase_date_ms" : "1637308628000", "purchase_date_pst" : "2021-11-18 23:57:08 America\/Los_Angeles", "quantity" : "1", "transaction_id" : "1000000914137632" } ], "original_application_version" : "1.0", "original_purchase_date" : "2013-08-01 07:00:00 Etc\/GMT", "original_purchase_date_ms" : "1375340400000", "original_purchase_date_pst" : "2013-08-01 00:00:00 America\/Los_Angeles", "receipt_creation_date" : "2021-11-19 07:57:08 Etc\/GMT", "receipt_creation_date_ms" : "1637308628000", "receipt_creation_date_pst" : "2021-11-18 23:57:08 America\/Los_Angeles", "receipt_type" : "ProductionSandbox", "request_date" : "2021-11-19 08:02:28 Etc\/GMT", "request_date_ms" : "1637308948244", "request_date_pst" : "2021-11-19 00:02:28 America\/Los_Angeles", "version_external_identifier" : 0 }, "status" : 0 } 复制代码
可以在此处找到本文的完整源代码。
结论
由于缺少大型 base64 接收参数,新的 App Store Connect API 为开发人员提供了更多信息并且运行速度更快。
新API的优点:
轻量级快速请求,通过
original_transaction_id
就够了。不再需要共享秘密。
还有一些额外的字段,如状态、类型。
新的 API 可用,例如从应用程序管理退款。
交易已在 API 中排序。
缺点:
相当复杂的请求授权:您需要生成 API Key 并从 App Store Connect 复制 Issuer ID。
交易价格仍然缺失。然而,Apphud 成功地计算了所有交易的价格,即使在如此困难的情况下,如升级期间按比例退款、价格上涨等。
作者:Fat君
链接:https://juejin.cn/post/7036663876611473415