(翻译)通过自己的服务器验证应用内购买

我们知道,苹果允许用户使用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. 服务器还会跟踪所有购买,以确保一次购买(在我们的情况下是应用的订阅)只使用一次。 换句话说,一次购买不能结束记入多个帐户。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,656评论 18 139
  • iOS应用内付费(IAP)开发步骤 1.苹果iTunes Connect内购产品信息录入。 1)创建app内购买项...
    MillerWang阅读 11,021评论 0 7
  • 后台服务器验证 IOS 内支付有两种模式: 1) 内置模式 2) 服务器模式 内置模式的流程可以简单的总结为以下几...
    月醉花听阅读 5,308评论 1 2
  • IOS 内支付有两种模式: 内置模式 服务器模式内置模式的流程可以简单的总结为以下几步: app从app stor...
    独酌丿红颜阅读 10,112评论 6 4
  • 说说拿到这本《一曲终了》的第一感觉。黑色的封面有点类似于大雨来临前的前奏曲的模样,黑沉沉的压得人有些发闷的紧张,...
    汪星人爱星辰阅读 771评论 0 49