ios 内购服务器验票(漏单处理)

1.漏单必须要处理,玩家花RMB购买的东西却丢失了,是绝对不能容忍的。所谓的漏单就是玩家已经正常付费,却没有拿到该拿的道具。

解决:只要购买成功,便将购买记录(receipt等账单信息)保存下来,然后将账单信息传送给我们游戏服务器,游戏服务器获得账单后,和苹果服务器验证,账单有效的话,回馈给游戏服务器处理,游戏服务器处理后,返回给游戏客户端处理,处理完毕,将本地保存的购买记录删除。

2.漏单的检测位置

解决:

2.1 做法1:在任意购买成功之后,顺便检测一次漏单,有漏单数遍处理了。

2.2 做法2:是在游戏登陆的时候检测一次漏单,即循环检测漏单数据,挨个发送给服务器验证处理,直到将所有的漏单处理完毕。这是原因是购买服务器未返回结果而客户端崩溃的情况下,玩家再次登陆,会产生漏单。

3.漏单的版本兼容

漏单要做好版本兼容,eg.玩家购买英雄ID为100的英雄,产生了一次漏单,但是一直未再次登陆游戏,由于版权等原因,这个英雄在后期版本中被删除了,如果玩家这是漏单处理,会在服务器获得一个丢弃的英雄,产生数据异常。

我的处理是,如果是英雄,检测英雄在本地hero.csv中是否有效,如果有效,检测这个英雄是否已经拥有,如果没有且数据正常,发送给服务器处理漏单,否则丢弃掉这条漏单。

还有说苹果服务器漏单过期的说法,不过我没有遇到过,没做处理。

4.服务器和客户端漏单对应顺序

遇到过这种情况,客户端产生了多个漏单,发送给游戏服务器验证,游戏服务器请求苹果服务,苹果服务器返回的receipt的json数据中包含一个所有未处理的订单列表,最后产生的购买数据在最后,客户端的漏单顺序和服务器的验证顺序要保持一致。

NOTE: The validated receipt may contain multiple transactions in the “in_app” parameter. It seems that Apple keeps all of the user’s transactions in the receipt in chronological order. Assuming users can only purchase one product at a time in your app, you want to grab the last transaction in the “in_app” array.

receipt的参数可以参考如下:

https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1

向苹果服务器验证收据返回的数据:

Status Description
0 The receipt provided is valid.
21000 The App Store could not read the JSON object you provided.
21002 The data in the receipt-data property was malformed.
21003 The receipt could not be authenticated.
21004 The shared secret you provided does not match the shared secret on file for your account.Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
21005 The receipt server is not currently available.
21006 This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
21007 This receipt is a sandbox receipt, but it was sent to the production server.
21008 This receipt is a production receipt, but it was sent to the sandbox server.


-------------------------------------------------------华丽丽的分割线-----------------------------------------------------------------------


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

说到IAP安全问题,在苹果的IAP流程中有一个比较明显的逻辑漏洞,这个逻辑漏洞是建立在我们处理不当的情况下发生的,会导致己方提供的服务和玩家之间出现问题。先看看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
voidpaymentQueue(...)
{
    if(transaction != nullptr)
    {
        me.money += 1000;
    }
}

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

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

但如果能伪造一个完全合法的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保存好,并且定期或在合理的UI界面触发向服务端发起请求,直至收到服务端的回复后删除客户端的receipt账单记录。这里就是我在开头提到的漏单处理了。

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

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

有朋友问怎么通过itunes-connect查看具体订单,itunes-connect中无法直接看到订单信息,可以用以下方法来查询

1.可以通过账单向苹果发送账单验证,有效可以手动补发

2 .用自己的服务器的记录账单列表对比 

3.利用第三方的TalkingData等交易函数,会自动记录账单数据


测试环境

在sandbox中验证receipt:https://sandbox.itunes.apple.com/verifyReceipt

在生产环境中验证receipt:https://buy.itunes.apple.com/verifyReceipt

那么如何自动的识别收据是否是sandbox receipt呢?
识别沙盒环境下收据的方法有两种:

  1. 根据收据字段 environment = sandbox。
  2. 根据收据验证接口返回的状态码。
    如果status=21007,则表示当前的收据为沙盒环境下收据, t进行验证。

苹果反馈的状态码:

  • 21000 App Store无法读取你提供的JSON数据
  • 21002 收据数据不符合格式
  • 21003 收据无法被验证
  • 21004 你提供的共享密钥和账户的共享密钥不一致
  • 21005 收据服务器当前不可用
  • 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
  • 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
  • 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证

注意:

在verifyWithRetry方法中,首先向向真实环境验证票据,如果是21007则向沙盒环境验证;==但是在消耗品类型的测试中,使用沙盒票据在真实环境中验证票据得到返回码:21002.所以下面代码在真实环境运行时,沙盒测试消耗型商品得不到正确的验证结果==。

/*
    verifyWithRetry the receipt
*/
    IAPVerifier.verifyWithRetry = function(receipt, isBase64, cb) {
        var encoded = null, receiptData = {};
        if (isBase64) {
            encoded = receipt;
        } else {
            encoded = new Buffer(receipt).toString('base64');
        }
        receiptData['receipt-data'] = encoded;
        var options = this.requestOptions();
        return this.verify(receiptData, options, (function(_this) {
            return function(error, data) {
            if (error) return cb(error);
            if ((21007 === (data != null ? data.status : void 0)) && (_this.productionHost == _this.host)) {
                var options_this.requestOptions();
                // 指向沙盒测试环境再次验证
                options.host = 'sandbox.itunes.apple.com/verifyReceipt';
                return _this.verify(receiptData, options, function(err, data) {
                    return cb(err, data);
                });
            } else {
                return cb(err, data);
          }
        };
      })(this));
    };


    /*
      verify the receipt data
     */

    IAPVerifier.verify = function(data, options, cb) {
        var post_data, request;
        post_data = JSON.stringify(data);
        options.headers = {
            'Content-Type': 'application/x-www-form-urlencoded', 
            'Content-Length': post_data.length
            };
        var request = https.request(options, (function(_this) {
            return function(response) {
                var response_chunk = [];
                response.on('data', function(data) {
                if (response.statusCode !== 200) {
                    return cb(new Error("response.statusCode != 200"));
                }
                response_chunk.push(data);
            });
            return response.on('end', function() {
                var responseData, totalData;
                totalData = response_chunk.join('');
                try {
                    responseData = JSON.parse(totalData);
                } catch (_error) {
                    return cb(_error);
                }
                return cb(null, responseData);
            });
        };
      })(this));
      request.write(post_data);
      request.end();
      request.on('error', function (exp) {
          console.log('problem with request: ' + exp.message);
      });
    };
    
    
    IAPVerifier.requestOptions = function() {
      return options = {
        host: 'buy.itunes.apple.com',
        port: 443,
        path: '/verifyReceipt',
        method: "POST",
        rejectUnauthorized: false/*不加:返回证书不受信任CERT_UNTRUSTED*/
      };
    };

建议:

为保证审核的通过,需要在客户端或server进行双重验证,即,先以线上交易验证地址进行验证,如果苹果正式验证服务器的返回验证码code为21007,则再一次连接沙盒测试服务器进行验证即可。在应用提审时,苹果IAP提审验证时是在沙盒环境的进行的,即:苹果在审核App时,只会在sandbox环境购买,其产生的购买凭证,也只能连接苹果的测试验证服务器,如果没有做双验证,需要特别注意此问题,否则会被拒。


阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页