1. 程式人生 > >ios 內購伺服器驗票(漏單處理)

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的引數可以參考如下:

向蘋果伺服器驗證收據返回的資料:

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呢?
識別沙盒環境下收據的方法有兩種:

  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環境購買,其產生的購買憑證,也只能連線蘋果的測試驗證伺服器,如果沒有做雙驗證,需要特別注意此問題,否則會被拒。