likes
comments
collection
share

[译]通过自己的服务器验证应用内购买

作者站长头像
站长
· 阅读数 10

我们知道,苹果允许用户使用Apple ID在应用程序中购买商品和服务。 所有的应用内购买都需要验证。 有两种方法可以验证这些购买: 1)通过App Store,通过您的应用程序和您的服务器之间的安全连接,或 2)本地。 本地验证可用于不需要服务器的简单应用程序。 但在这里你遇到安全风险:本地购买可以伪造的黑客iPhone。 因此,使用自己的服务器与App Store进行通信通常是最好的选择。 在这种情况下,您的应用程序将只识别并信任您的服务器,让您控制服务器和用户设备之间的所有交易。 在本文中,我们将通过我们的服务器分享我们验证购买的专业知识。 我们在我们的一个项目中实现了这个功能,一个约会应用程序叫做Bro。

###什么是Bro?

Bro是一款男性同性恋交友APP。 Bro具有丰富的功能,并提供两种类型的应用内购买: . 一个月,六个月和每年订阅。 订阅者获得无广告的体验,可以看到更多的潜在匹配,并在本地用户中显示更多(最多200个)匹配。 . 一个“Bromance”功能,像Tinder的超级喜欢。

当我们在Bro中使用应用内购买时,我们必须处理一些我们想要与您分享的挑战。 产品的应用程序提供包括高级订阅(可再生),高级订阅(不可再生),消费品(bromances)。

在我们开始开发我们的采购系统之前,我们研究了使用我们自己的服务器进行验证的好处。 这里是我们想出了:

  1. 服务器端验证比本地验证更安全。
  2. 我们已经有了自己的服务器。
  3. 如果用户是管理员,他们可以访问某些高级功能,而无需购买。
  4. 由于我们同时拥有Android和iOS应用程序,我们需要在两个操作系统上跟踪高级用户状态,这在单个服务器上最方便。

###关于购买收据的一些特殊信息:

收据是包含有关购买的所有信息的文件,包括购买日期,到期日期,原始购买ID,产品ID等。 苹果最近推出了新的收据格式 - 通用收据。 以前,您将收到每个交易的单独收据(即一个交易等于一个收据)。 但现在,他们只发送一个收据,其中包含有关给定Apple ID的所有购买的所有信息,包括已完成和未完成的购买。 通用收据的另一个变化是,它们是从mainBundle()使用appStoreReceiptURL变量下载的,而不是来自事务本身。

###Flow: 用户购买订阅或可消费产品。 当购买完成,苹果发送一个收据,我们可以从mainBundle()访问当我们收到收据,我们然后作为一行代码发送到我们的服务器。

let mainBundle = NSBundle.mainBundle()
let receiptUrl = mainBundle.appStoreReceiptURL
let isPresent = receiptUrl?.checkResourceIsReachableAndReturnError(nil)
if isPresent == true, let receiptUrl = receiptUrl,  receipt = NSData(contentsOfURL: receiptUrl) {
   let receiptData = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
} 
//receiptData - encoded string for our server

当此收据到达我们的服务器时,它由App Store验证,并且如果有效,则将信息发送到客户端的设备,例如关于在服务器侧的客户端状态的改变,或关于由 客户。 在Bro应用程序中,当客户端成为高级用户或支付附加服务时,这些操作将通过服务器运行。 这意味着,有关用户购买状态的所有信息(例如,他们是否有高级帐户)在服务器上始终是最新的。 但作为一个附加层 - 服务器 - 是在客户端和购买之间引入的,我们必须考虑购买已经完成但服务器尚未收到任何信息的情况 - 用户已经关闭了 app。 例如,如果互联网连接中断,或者设备的电池电量耗尽,则可能会发生这种情况。 苹果在其技术文档中明确指出,在购买之后,其状态必须设置为“完成”,然后过程才能被视为完成。 在我们的情况下,我们只能在客户端设备收到来自服务器的响应后,将购买标记为“已完成”,表明服务器成功传输和验证了所有数据。

private func validateReceipt(transaction: SKPaymentTransaction, isRestoring: Bool = false) { 
   let mainBundle = NSBundle.mainBundle()
   let receiptUrl = mainBundle.appStoreReceiptURL
   let isPresent = receiptUrl?.checkResourceIsReachableAndReturnError(nil)
   if isPresent == true, let receiptUrl = receiptUrl, receipt = NSData(contentsOfURL: receiptUrl) { 
          let receiptData = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0)) 
          configureRestoringRequest( parameters, completion: { 
             SKPaymentQueue.defaultQueue().finishTransaction(transaction)                    // finish transaction only when response from server is received
     } )
 } else { 
  // handle case when there is no receipt data
   }
 }
 private func configureValidationRequest(receiptData: String, completion: () -> ()) {
 APIClient.defaultClient().validatePremium( receiptData, success: {[weak self] _, _ in completion()
 }, failure: {[weak self] _, error in
 // handle errors here 
  } ) 
}

如果购买的状态为已购买或已恢复,则该状态将保留在付款队列中进行处理,直到状态更改为已完成或已取消。

func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
       for transaction in transactions { 
          switch transaction.transactionState { 
          case .Purchased: // handle transaction is Purchased if needed case .Restored: // handle transaction is Restored if needed 
          case .Purchasing: // handle transaction purchasing is in progress if needed case .Failed: // handle transaction failure if needed
          default:
               break 
          }
     }
 }

这就是为什么我们在每次启动应用程序时激活我们的Purchase服务,所以我们可以处理由于某种原因或没有成功发送到服务器的任何交易。

// call this function when your session starts 
private func setupPurchaseService() {       
     BroPurchaseService.sharedPurchasingService.setupPurchasingService() 
} 
private func checkStoreAvailability() {     
      SKPaymentQueue.defaultQueue().addTransactionObserver(self)
     if SKPaymentQueue.canMakePayments() {
     let productID = NSSet(objects: productOneId, productTwoId, etc) // add all products ids 
     let request = SKProductsRequest(
     productIdentifiers: productID as! Set<String>)
     request.delegate = self 
     request.start()
     } else { 
      // handle cases when your app can't make payments
   }
 }

此外,通过App Store的所有可续订订阅在验证阶段请求共享密钥。 在验证期间,此密钥必须与收据一起发送,并且此密钥对于应用程序的所有用户都是相同的。 为了节省时间并避免将密钥从客户端发送到服务器,我们将其存储在服务器上,并仅发送回执。 这比为每个事务将密钥发送到服务器更安全。 如果密钥必须被替换(出于安全原因,例如如果数据被泄露),则管理员可以通过生成新密钥并且使用它们自己的简档用新密钥替换旧密钥来替换密钥。 当用户成功购买产品时,服务器将让我们知道它,无论他们登录什么设备。 此外,服务器将自动检查其在App Store上的订阅状态,并在高级功能已过期时限制高级功能。

###有关恢复订阅的一些提示: 当您恢复购买时,所有已完成的交易都将转到具有已恢复状态的付款队列。 这意味着我们必须验证并尝试恢复每个事务; 但如果我们在SandBox环境中还原它们,则可能需要5分钟才能续订1个月的订阅,并且可能需要30分钟才能续订6个月的订阅。 6个不同的交易可能需要30分钟! 我们决定在恢复每个交易时不发送请求,而是向包含所有购买信息的服务器发送一个收据。 然后,Apple在其服务器上验证收据,检查所有事务并恢复那些需要恢复的事务。 一旦服务器发送确认交易成功,所有先前状态为Restored的交易将收到状态完成。

func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
   var restoreBegan = false 
  for transaction in transactions { 
      switch transaction.transactionState {
      case .Purchased: // handle transaction is Purchased if needed
      case .Restored:
         if !restoreBegan {
                 restoreBegan = true validateReceipt(transaction) // validate receipt as it is purchased
   } else {     
        SKPaymentQueue.defaultQueue().finishTransaction(transaction) 
    } 
      case .Purchasing: // handle transaction purchasing is in progress if needed
      case .Failed: // handle transaction failure if needed 
      default: 
            break 
    }
   }
 }

此外,如果用户尝试在尝试失败后再次尝试购买订阅,Apple将替换地提供这些订阅以恢复其现有订阅,而不是完成新购买。 还有一个不寻常的情况需要考虑:如果用户尝试购买两次产品,它会再次显示在付款队列中,但也会恢复,这意味着用户不会被收取新的购买费用。 苹果的新通用收据允许我们通过只向服务器发送一个请求来执行所有操作,因为它包含所有以前事务的所有信息。

###我们还想提到一些我们注意到的常见的沙盒陷阱:

  1. 在我们的其中一个测试设备上,我们有时会收到来自多个测试帐户的交易,包括已删除Apple ID的帐户。
  2. 收据可能大到100 kb(当与某些类型的服务器交互时会导致麻烦)。
  3. 一些购买结果是恢复状态,虽然这不应该发生根据项目的技术规格。
  4. 苹果服务器的响应时间很慢。
  5. 有时,购买没有出现在队列中 - 我们没有得到苹果的任何回调。

此外,根据我们的经验,我们建议在一台设备上使用一个Apple ID测试一个用户。

让我们总结一下我们在应用内购买的经验。

###使用App Store验证收据相对于本地验证具有以下优点:

  1. 所有信息在服务器端验证,这意味着无论运行应用程序的设备是什么,用户将获得有关购买的最新数据。
  2. 服务器还会跟踪所有购买,以确保一次购买(在我们的情况下是应用的订阅)只使用一次。 换句话说,一次购买不能结束记入多个帐户。