InApp 购买耗材服务器验证设置
InApp Purchase Consumable Server Validation Set Up
我正在使用 SwiftyStoreKit
作为小费罐的 InApp 购买消耗品。一切都适用于测试,但根据 this answer 和评论服务器端验证不是必需的,但建议这样做。答案是 "consumables, un-consumables and subscriptions are susceptible to fraudulent attacks. Often though iap crackers or network spoofing. Validating the receipt can mitigate this problem"
.
1- 如果用户 A 向我发送小费,如果一切都通过 Apple,攻击者怎么可能拦截该小费并拿走钱?
2- 我需要设置一个 Heroku 实例还是使用其他东西来进行服务器验证?我在上面找不到任何东西。我假设我需要在 if product.needsFinishTransaction { SwiftyStoreKit.finishTransaction(product.transaction) }
下面的 success case
中添加服务器端代码,但我不知道如何从那时起设置服务器。
SwiftyStoreKit.purchaseProduct(product, quantity: 1, atomically: true) { result in
switch result {
case .success(let product):
// fetch content from your server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("Purchase Success: \(product.productId)")
// failed cases ...
}
}
代码如下:
AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
for purchase in purchases {
switch purchase.transaction.transactionState {
case .purchased, .restored:
if purchase.needsFinishTransaction {
// Deliver content from server, then:
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
// Unlock content
case .failed, .purchasing, .deferred:
break // do nothing
@unknown default:
break
}
}
}
}
TipJarVC。购买是在 collectionView's didSelect item
:
var dataSource = [Tip]()
var sharedSecret = appStoreConnectSecretKey
let inAppProductIds = ["com.myCo.myAppName.firstTip", // 0.99
"com.myCo.myAppName.secondTip", // 9.99 ]
override func viewDidLoad() {
super.viewDidLoad()
getInAppPurchaseAmounts()
}
func getInAppPurchaseAmounts() {
// show spinner
let dispatchGroup = DispatchGroup()
for productId in inAppProductIds {
dispatchGroup.enter()
SwiftyStoreKit.retrieveProductsInfo([productId]) { [weak self](result) in
if let product = result.retrievedProducts.first {
let priceString = product.localizedPrice!
print("Product: \(product.localizedDescription), price: \(priceString)")
let tip = Tip(displayName: product.description,
description: product.localizedDescription,
productId: productId
price: priceString)
self?.addTipToDataSource(tip)
if let sharedSecret = self?.sharedSecret {
self?.verifyPurchase(with: productId, sharedSecret: sharedSecret)
}
dispatchGroup.leave()
} else if let invalidProductId = result.invalidProductIDs.first {
print("Invalid product identifier: \(invalidProductId)")
dispatchGroup.leave()
} else {
print("Error: \(String(describing: result.error))")
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .global(qos: .background)) { [weak self] in
DispatchQueue.main.async { [weak self] in
// removeSpinnerAndReloadData()
}
}
}
func verifyPurchase(with productId: String, sharedSecret: String) {
let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: sharedSecret)
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
switch result {
case .success(let receipt):
let productId = productId
// Verify the purchase of Consumable or NonConsumable
let purchaseResult = SwiftyStoreKit.verifyPurchase(
productId: productId,
inReceipt: receipt)
switch purchaseResult {
case .purchased(let receiptItem):
print("\(productId) is purchased: \(receiptItem)")
case .notPurchased:
print("The user has never purchased \(productId)")
}
case .error(let error):
print("Receipt verification failed: \(error)")
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? TipCell else { return }
guard let indexPath = collectionView.indexPath(for: cell) else { return }
let tip = dataSource[indexPath.item]
purchaseProduct(with: tip.productId)
}
func purchaseProduct(with productId: String) {
SwiftyStoreKit.retrieveProductsInfo([productId]) { result in
if let product = result.retrievedProducts.first {
SwiftyStoreKit.purchaseProduct(product, quantity: 1, atomically: true) { result in
switch result {
case .success(let product):
// fetch content from your server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("Purchase Success: \(product.productId)")
case .error(let error):
switch error.code {
case .unknown:
print("Unknown error. Please contact support")
case .clientInvalid:
print("Not allowed to make the payment")
case .paymentCancelled:
print("Payment cancelled")
case .paymentInvalid:
print("The purchase identifier was invalid")
case .paymentNotAllowed:
print("The device is not allowed to make the payment")
case .storeProductNotAvailable:
print("The product is not available in the current storefront")
case .cloudServicePermissionDenied:
print("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed:
print("Could not connect to the network")
case .cloudServiceRevoked:
print("User has revoked permission to use this cloud service")
default:
print((error as NSError).localizedDescription)
}
}
}
}
}
}
你不必害怕攻击者——因为它只与越狱设备有关,他们唯一能做的就是免费使用你的付费功能。而且没有人能拿走这笔钱。一切安好
如果您只想进行 IAP 购买,您可以使用 SwiftyStoreKit
。
您可以从我们的博客中阅读 this article 关于收据验证的内容。
我正在使用 SwiftyStoreKit
作为小费罐的 InApp 购买消耗品。一切都适用于测试,但根据 this answer 和评论服务器端验证不是必需的,但建议这样做。答案是 "consumables, un-consumables and subscriptions are susceptible to fraudulent attacks. Often though iap crackers or network spoofing. Validating the receipt can mitigate this problem"
.
1- 如果用户 A 向我发送小费,如果一切都通过 Apple,攻击者怎么可能拦截该小费并拿走钱?
2- 我需要设置一个 Heroku 实例还是使用其他东西来进行服务器验证?我在上面找不到任何东西。我假设我需要在 if product.needsFinishTransaction { SwiftyStoreKit.finishTransaction(product.transaction) }
下面的 success case
中添加服务器端代码,但我不知道如何从那时起设置服务器。
SwiftyStoreKit.purchaseProduct(product, quantity: 1, atomically: true) { result in
switch result {
case .success(let product):
// fetch content from your server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("Purchase Success: \(product.productId)")
// failed cases ...
}
}
代码如下:
AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
for purchase in purchases {
switch purchase.transaction.transactionState {
case .purchased, .restored:
if purchase.needsFinishTransaction {
// Deliver content from server, then:
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
// Unlock content
case .failed, .purchasing, .deferred:
break // do nothing
@unknown default:
break
}
}
}
}
TipJarVC。购买是在 collectionView's didSelect item
:
var dataSource = [Tip]()
var sharedSecret = appStoreConnectSecretKey
let inAppProductIds = ["com.myCo.myAppName.firstTip", // 0.99
"com.myCo.myAppName.secondTip", // 9.99 ]
override func viewDidLoad() {
super.viewDidLoad()
getInAppPurchaseAmounts()
}
func getInAppPurchaseAmounts() {
// show spinner
let dispatchGroup = DispatchGroup()
for productId in inAppProductIds {
dispatchGroup.enter()
SwiftyStoreKit.retrieveProductsInfo([productId]) { [weak self](result) in
if let product = result.retrievedProducts.first {
let priceString = product.localizedPrice!
print("Product: \(product.localizedDescription), price: \(priceString)")
let tip = Tip(displayName: product.description,
description: product.localizedDescription,
productId: productId
price: priceString)
self?.addTipToDataSource(tip)
if let sharedSecret = self?.sharedSecret {
self?.verifyPurchase(with: productId, sharedSecret: sharedSecret)
}
dispatchGroup.leave()
} else if let invalidProductId = result.invalidProductIDs.first {
print("Invalid product identifier: \(invalidProductId)")
dispatchGroup.leave()
} else {
print("Error: \(String(describing: result.error))")
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .global(qos: .background)) { [weak self] in
DispatchQueue.main.async { [weak self] in
// removeSpinnerAndReloadData()
}
}
}
func verifyPurchase(with productId: String, sharedSecret: String) {
let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: sharedSecret)
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
switch result {
case .success(let receipt):
let productId = productId
// Verify the purchase of Consumable or NonConsumable
let purchaseResult = SwiftyStoreKit.verifyPurchase(
productId: productId,
inReceipt: receipt)
switch purchaseResult {
case .purchased(let receiptItem):
print("\(productId) is purchased: \(receiptItem)")
case .notPurchased:
print("The user has never purchased \(productId)")
}
case .error(let error):
print("Receipt verification failed: \(error)")
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? TipCell else { return }
guard let indexPath = collectionView.indexPath(for: cell) else { return }
let tip = dataSource[indexPath.item]
purchaseProduct(with: tip.productId)
}
func purchaseProduct(with productId: String) {
SwiftyStoreKit.retrieveProductsInfo([productId]) { result in
if let product = result.retrievedProducts.first {
SwiftyStoreKit.purchaseProduct(product, quantity: 1, atomically: true) { result in
switch result {
case .success(let product):
// fetch content from your server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("Purchase Success: \(product.productId)")
case .error(let error):
switch error.code {
case .unknown:
print("Unknown error. Please contact support")
case .clientInvalid:
print("Not allowed to make the payment")
case .paymentCancelled:
print("Payment cancelled")
case .paymentInvalid:
print("The purchase identifier was invalid")
case .paymentNotAllowed:
print("The device is not allowed to make the payment")
case .storeProductNotAvailable:
print("The product is not available in the current storefront")
case .cloudServicePermissionDenied:
print("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed:
print("Could not connect to the network")
case .cloudServiceRevoked:
print("User has revoked permission to use this cloud service")
default:
print((error as NSError).localizedDescription)
}
}
}
}
}
}
你不必害怕攻击者——因为它只与越狱设备有关,他们唯一能做的就是免费使用你的付费功能。而且没有人能拿走这笔钱。一切安好
如果您只想进行 IAP 购买,您可以使用 SwiftyStoreKit
。
您可以从我们的博客中阅读 this article 关于收据验证的内容。