这两天我在筹备我们的游戏APP的内购,仔细考虑了几个付费安全上的问题。凡是涉及到付费的问题都很敏感,任何一方出现损失都是不能接受的,所以在这里整理一些支付安全的要点分享一下。IAP是指In-App Purchase, 是一种付费方式,而并不是苹果专有的付费方式,在其它平台上也会有不同的实现,这里针对Apple IAP。

说到IAP安全问题,在苹果的IAP流程中有一个比较明显的逻辑漏洞,这个逻辑漏洞是建立在我们处理不当的情况下发生的,会导致己方提供的服务和玩家之间出现问题。先看看IAP支付时序图:
sequence_iap

整个支付流程如下:
1.客户端向Appstore请求购买产品(假设产品信息已经取得),Appstore验证产品成功后,从用户的Apple账户余额中扣费。
2.Appstore向客户端返回一段receipt-data,里面记录了本次交易的证书和签名信息。
3.客户端向我们可以信任的游戏服务器提供receipt-data
4.游戏服务器对receipt-data进行一次base64编码
5.把编码后的receipt-data发往itunes.appstore进行验证
6.itunes.appstore返回验证结果给游戏服务器
7.游戏服务器对商品购买状态以及商品类型,向客户端发放相应的道具与推送数据更新通知

这七个步骤实际上是一个很安全的流程了。那问题出在哪里呢?我们谈谈两种苹果IAP的验证模型。

IAP built-in Model,本地验证

有些单机游戏甚至是网游,都直接跳过了3~7步骤,在第2步拿到receipt-data之后,直接由客户端向itunes.appstore发送验证请求,并且拿到结果,根据结果修改游戏数据。

我们在设计游戏的时候都遵循一个真理,“凡是在客户端的数据都是不安全的”,深以为然。如果没有独立服务器辅助验证,这样也就避免不了数据被修改的事实了,是的,你会少赚钱。不过如果网游也不通过独立服务器验证,而是在客户端验证之后再告知服务器状态让其发放游戏道具,那就太可怕了点。这是IAP built-in Model,经常出现安全问题的逻辑如下:

1
2
3
4
5
6
7
void paymentQueue(...)
{
if (transaction != nullptr)
{
me.money += 1000;
}
}

上面的代码在接收到付费成功的response就直接给游戏发放商品,不对产品和单据进行验证。如果receipt-data允许放在本地验证,就可能发生我们说的免费内购的BUG. 而实际上也真的有类似IAPCracker/IAPFree等工具专门利用这样的IAP漏洞的。而对于已经越狱了的iOS设备就太简单了,甚至不需要通过伪造或者跳过receipt-data验证就可以修改本地数据达到目的。

那是不是就完全不能让这个过程变得安全了呢?也不是,但这个安全保障只是让修改变得困难而已。苹果官方提供了 Validating Receipts Locally 在客户端对receipt-data进行安全验证,主要是对证书以及签名的合法性验证。如果不想自己写代码验证,也可以借助第三方机构提供的receipt-data验证API,比较著名的有 urbanairshipbeeblex

但如果能伪造一个完全合法的receipt-data,是不是一样可以达到欺骗目的。是的,为了绕过Validating Locally,于是黑客开始用自己伪造的receipt-data进行移花接木,所以出现了可以伪造”合法订单”的 in-appstore 。因此这种本地加强验证的方法也不能完全避免IAP攻击。

IAP Server Model,服务器验证

而如果我们把验证逻辑移到服务器上,这个过程就变得容易多了。因为不再需要担心receipt-data被伪造的问题。不过就算把步骤4~7在服务器上做了,同样也会产生一些幼稚的逻辑漏洞:

对验证receipt-data的reponse content不进行验证和记录,只根据Product直接发放商品。这样只要客户端不断提交receipt-data,按照正常逻辑你就需要不断验证并且重复发放商品。较为安全的做法是:

在每一次收到receipt-data之后,都把提交的玩家账号以及receipt-data中的单号建立映射并记录下来,在每次验证receipt-data时,先判断其是否已经存在。

只要做了这样的验证,整个支付流程都变得明朗起来。

确保receipt-data的成功提交与异常处理

建立在IAP Server Model的基础上,并且我们知道手机网络是不稳定的,在付款成功后不能确保把receipt-data一定提交到服务器。如果出现了这样的情况,那就意味着玩家被appstore扣费了,却没收到服务器发放的道具。

解决这个问题的方法是在提交receipt-data的协议上设一个返回值,让服务端告知它已经成功收到并验证了receipt-data. 在没有收到这样的回复之前,客户端必须要把receipt-data保存好,并且定期向服务端发起请求,直至收到服务端的回复为止。

如果是客户端没成功提交receipt-data,那怎么办?就是玩家被扣费了,也收到appstore的消费收据了,却依然没收到游戏道具,于是投诉到游戏客服处。

这种情况在以往的经验中也会出现,常见的玩家和游戏运营商发生的纠纷。游戏客服向玩家索要游戏账号和appstore的收据单号,通过查询itunes-connect看是否确有这笔订单。如果订单存在,则要联系研发方去查询游戏服务器,看订单号与玩家名是否对应,并且是否已经被使用了,做这一点检查的目的是为了防止恶意玩家利用已经使用过了的订单号进行欺骗,谎称自己没收到商品。这就是上面一节IAP Server Model中红字所提到的安全逻辑的目的。当然了,如果查不到这个订单号,就意味着这个订单确实还没使用过,手动给玩家补发商品即可。