前言:先说IAP的事,本来咱们的支付功用需求S2S验证收据的真实性。但是曾经的收据[appStoreReceiptURL]在 StoreKit2中无法获取。
查阅了现在收据原来是在JWS中,但是发现这个JWS咱们后台验证还没开端实现(#_#)。
没办法,一同找啊找,各种关键词: StoreKit2 verify AppleDeveloperForums…丢人就不念了。
直到遇见了这篇文章 转载一下,希望也能够解决你现在遇到的问题。
正文开端
App Store Server API – 是一种新的 REST API,可让您获取有关一切客户运用内购买的信息。与旧的 verifyReceipt 端点的主要区别在于,您不再需求向服务器发送大型 base64 收据。`
检索信息是运用原始事务 ID 完成的,恳求和呼应都运用 JWT 和从 App Store Connect 生成的 API 密钥进行签名。
生成运用内购买 API 密钥
为运用内购买生成密钥与生成订阅密钥相同——该选项卡已被简单地重命名
要为运用内购买生成 API 密钥,请拜访:
- 用户和拜访
- 钥匙
- 在运用程序内购买
下载密钥并将其保存到安全的当地。请注意,您只能下载一次密钥。
发行人编号
要创立恳求,您还需求颁布者 ID,能够在Keys > App Store Connect API选项卡中找到它。如果页面上短少此字段,您可能需求创立您的第一个 App Store Connect API 密钥,即使您不会运用它。您也能够测验从一切者帐户签名。
创立 JWT
JSON Web Token (JWT) 运用敞开规范RFC 7519,该规范界说了一种安全传输信息的方式。
生成令牌运用 3 个步骤完成:
1. 创立 JWT 标头
1. 创立 JWT 负载
1. 签署 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
编写 Demo:【PS: php我最讨厌的言语】
让咱们创立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
}
//JWS解析后
{
"transactionId":"10000009269223342",
"originalTransactionId":"1000003316922942",
"bundleId":"com.---.ios",
"productId":"king.test.gold.60",
"purchaseDate":1637723816809,
"originalPurchaseDate":1637723816809,
"quantity":1,
"type":"Consumable",
"deviceVerification":"qVh9...+B3fIAoQKL8Kz0CkmVGfUiwpPrfcGdlJyht775ID9ytSQCWItx",
"deviceVerificationNonce":"a8735bcf-825f-4aeb-b99c-6f866cadc96e",
"appAccountToken":"3977...-61b8-bfb7-c94f-8ea670fdb7b7",
"inAppOwnershipType":"PURCHASED",
"signedDate":1637723816914
}
能够在此处找到本文的完整源代码。
结论
由于短少大型 base64 接收参数,新的 App Store Connect API 为开发人员提供了更多信息而且运行速度更快。
新API的优点:
- 轻量级快速恳求,通过
original_transaction_id
就够了。 - 不再需求共享秘密。
- 还有一些额外的字段,如状况、类型。
- 新的 API 可用,例如从运用程序管理退款。
- 买卖已在 API 中排序。
缺陷:
- 相当杂乱的恳求授权:您需求生成 API Key 并从 App Store Connect 仿制 Issuer ID。
- 买卖价格仍然缺失。但是,Apphud 成功地计算了一切买卖的价格,即使在如此困难的情况下,如晋级期间按比例退款、价格上涨等。