IAP详解
In-App Purchase Walk Through
1. 適用情況
想使用In-App Purchase(以下簡稱IAP)完成App內付費前,先確定需求是不是能用這個方案來滿足。
除了IAP以外,還有支付寶SDK、信用卡等其他方式完成軟件內付費。
在蘋果制定的游戲規則中,所有在App內提供的服務需要付費時,都應當使用IAP,比如軟件功能、游戲道具;所有在App外提供的服務需要付費時,都應使用其他支付方式,比如Uber的信用卡,淘寶、快的打車的支付寶SDK等。
在IAP里,可以出售:
在IAP里,不能出售:
順便說下,有次大網易的同事分享時提到:使用兌換碼兌換App內服務是一條高壓線。像Uber和Amazon里允許有碼,是因為他們的碼是用在現實世界的產品或服務上的。
如果你確定內購需求符合IAP的使用要求,可以繼續往下讀了。
2. 購買及發放虛擬產品流程
官方給出的流程圖是這樣的:
3.虛擬產品
虛擬產品的分類
虛擬產品分為以下幾種類型:
類型2、3、5都是以Apple ID為粒度的。比如小張有三個iPad,有一個Apple ID購買了不可消耗品,則三個iPad上都可以使用。
類型1、4一般來說則是現買現用。如果開發者自己想做更多控制,一般選4。
幾種產品的區別如下(表格懶得翻譯了):
Table 1-1 Comparison of product types
| Users can buy? | Once? | Multiple times |
| Appears in the receipt | Always | Once |
| Synced across devices | By the system | Not synced |
| Restored | By the system | Not restored |
Table 1-2 Comparison of subscription types
| Users can buy | Multiple times | Multiple times | Once |
| Appears in the receipt | Always | Once | Always |
| Synced across devices | By the system | By your app | By the system |
| Restored | By the system | By your app | By the system |
關于自動更新訂閱品更新周期組(Auto-Renewable Subscription Duration Families):
每種訂閱品的每種更新周期可以在iTunes Connect中設置一個單獨的價格。圖中給出了一種訂閱品的不同長度的更新周期的價格截圖:
你可以把每種訂閱品的每個長度的更新周期看成一個單獨的產品,每個產品有自己獨有的時長、價格、市場促銷屬性。
為了讓用戶可以從中挑選一個偏好的更新周期,上圖中我們為此種訂閱的每個長度的更新周期分別設值了一個單獨的價格,有一周的、一個月的、兩個月的、季度的、半年的和一年的。
上圖中這種訂閱品的全部六種更新周期合起來稱為一個自動更新訂閱品更新周期組(Auto-Renewable Subscription Duration Families)。
4. 人肉和iTunes Connect交互
填寫銀行卡與納稅信息
即使信息正在審核,沙箱環境下也是可以訪問IAP服務的,并不需要等審核完成才能測試。
新建虛擬產品
新建完,不用等待蘋果審核就可以在沙箱環境使用了。
新建測試帳號
附:在蘋果托管不可消耗品(Non-consumable products)的內容需知
托管內容僅限于針對不可消耗品。
首次創建不可消耗品時可以選擇把內容托管到蘋果服務器,當然也可以隨時將自己服務器上的內容遷移到蘋果服務器由蘋果托管。
需要使用托管功能的話,首先在iTunes Connect中提交不可消耗品讓蘋果審核。然后在Xcode中選取In-App Purchase Content template創建虛擬產品, 放入需要托管的內容, 然后使用Archive功能上傳。或者使用Xcode為每一種虛擬產品創建一個.pkg文件,然后使用Application Loader一次性上傳。
具體細節請參考Using Application Loader中和In-App Purchase有關的章節。
關于和iTunes Connect的交互,更多細節請參考In-App Purchase Configuration Guide for iTunes Connect。
5. 代碼里該做的事情
獲取產品列表
| 1 2 3 4 5 6 7 | #import <StoreKit/StoreKit.h> #define kInAppPurchaseProUpgradeProductId @"com.163.neteasemusic.skin.dog" ... NSSet *productIDs = [NSSet setWithObject:kInAppPurchaseProUpgradeProductId]; SKProductsRequest *request= [[SKProductsRequest alloc] initWithProductIdentifiers:productIDs]; request.delegate = self; [request start]; |
接收結果
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { NSArray *myProducts = response.products; for (SKProduct *product in myProducts) { //product } } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error { //處理錯誤 } |
向自己的服務器生成訂單
如果需要經過自己的服務器做二次驗證,建議在調用蘋果支付接口前做這一步。
訂單中必須要保存的是訂單ID和用戶想要購買的商品ID。這個記錄是為了在二次驗證時服務端做檢查,防止 A 商品的 receipt 被用戶拿來做 B 商品的購買結果校驗。
發送購買請求
| 1 2 3 4 5 6 | #import <StoreKit/StoreKit.h> ... SKProduct *product = <# products request中返回的SKProduct #>; SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; payment.quantity = 2; [[SKPaymentQueue defaultQueue] addPayment:payment]; |
或者
| 1 2 3 4 5 | #import <StoreKit/StoreKit.h> ... SKProduct *product = <# products request中返回的SKProduct #>; SKPayment *payment = [SKPayment paymentWithProduct:product]; [[SKPaymentQueue defaultQueue] addPayment:payment]; |
觀察購買狀態
首先在程序啟動時注冊觀察者
| 1 2 3 | #import <StoreKit/StoreKit.h> ... [[SKPaymentQueue defaultQueue] addTransactionObserver:observer]; |
并且實現回調,處理相應的購買返回。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { // Call the appropriate custom method for the transaction state. case SKPaymentTransactionStatePurchasing: [self showTransactionAsInProgress:transaction deferred:NO]; break; case SKPaymentTransactionStateDeferred: [self showTransactionAsInProgress:transaction deferred:YES]; break; case SKPaymentTransactionStateFailed: [self failedTransaction:transaction]; break; case SKPaymentTransactionStatePurchased: [self completeTransaction:transaction]; break; case SKPaymentTransactionStateRestored: [self restoreTransaction:transaction]; break; default: // For debugging NSLog(@"Unexpected transaction state %@", @(transaction.transactionState)); break; } } } |
需要監聽SKPaymentQueue的更多狀態變更,請實現SKPaymentTransactionObserver協議中提供的更多方法。
完成購買
在收到Purchased或Restored回調后,持久化購買記錄以及receipt data。iOS6.X 或之前的版本中,持久化 receipt data 必須萬無一失,因為一旦丟失,將沒有任何途徑再次拿到此 receipt,造成用戶購買記錄丟失。獲取 receipt data 需要注意的點將在后面的二次驗證中詳細說。
然后通知PaymentQueue,購買已經完成了。對finishTransaction則會觸發系統IAP的UI刷新:
| 1 2 | SKPaymentTransaction *transaction = <# The current payment #>; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; |
另外在發放功能或道具之前,最好在自己服務端做一次二次校驗,防止越獄插件或者Wifi的HTTP代理偽造購買記錄。
二次驗證防止破解
越獄插件或者HTTP代理均可讓用戶做到偽造購買記錄。當我們收到購買完成的回調后,最好經過自己服務器驗證購買是否合法。
經過 App Store 驗證
以下代碼用Cocoa實現了二次驗證的過程。但是這個過程最好通過自己的后臺服務器來做,不然非常容易在客戶端被偽造返回結果。
這里使用Cocoa實現只是為了闡述請求與返回值的格式。 發送二次驗證請求:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | #define SANDBOX_VERIFY_RECEIPT_URL [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"] #define APP_STORE_VERIFY_RECEIPT_URL [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"] #ifdef DEBUG #define VERIFY_RECEIPT_URL SANDBOX_VERIFY_RECEIPT_URL #else #define VERIFY_RECEIPT_URL APP_STORE_VERIFY_RECEIPT_URL #endif ... - (void)verifyTransaction:(SKPaymentTransaction *)transaction { NSData *transactionReceipt = [[self class]] receiptDataFromTransaction:transaction]; NSString *base64String = [OTBase64Helper base64forData:transactionReceipt]; NSDictionary *receiptDictionary = @{@"receipt-data":base64String}; NSData *data = [receiptDictionary JSONData]; if (_receiptRequest) { [_receiptRequest cancel]; _receiptRequest = nil; } _receiptRequest = [[ASIFormDataRequest alloc] initWithURL:VERIFY_RECEIPT_URL]; _receiptRequest.userInfo = @{@"ProductIdentifier" : transaction.payment.productIdentifier}; _receiptRequest.delegate = self; [_receiptRequest appendPostData:data]; [_receiptRequest startAsynchronous]; } + (NSData *)receiptDataFromTransaction:(SKPaymentTransaction *)transaction { NSData *receiptData = [self receiptDataInReceiptURL]; if (!receiptData) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" if ([transaction respondsToSelector:@selector(transactionReceipt)]) { //Works in iOS3 - iOS8, deprected since iOS7, actual deprecated (returns nil) since iOS9 receiptData = transaction.transactionReceipt; } #pragma clang diagnostic pop } return receiptData; } + (NSData *)receiptDataInReceiptURL { if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0") && [[NSBundle mainBundle] respondsToSelector:@selector(appStoreReceiptURL)]) { //Works since iOS7, implemented but calls selector not found directly in iOS6 //so must decide by if system version >= 7.0, DO NOT use respondsToSelector:@selector(appStoreReceiptURL) NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL]; if ([[NSFileManager defaultManager] fileExistsAtPath:[receiptUrl path]]) { NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl]; return receiptData; } } return nil; } |
需要注意的是,如果 App 不需要支持 iOS6.x 及之前的版本,建議僅使用 appStoreReceiptURL 獲取 receipt 數據。appStoreReceiptURL 從 iOS7 開始啟用,會返回用戶在此 app 上購買過的全部 receipt 的 data(粒度猜測應該是本機,本 App Store 帳號,本 App 內的購買,具體沒測試)。
接收二次驗證結果:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | - (void)requestFinished:(ASIHTTPRequest *)request { NSString *responseString = [request responseString]; NSDictionary *dictionary = [responseString objectFromJSONString]; NSString *productId = dictionary[@"receipt"][@"product_id"]; NSNumber *status = dictionary[@"status"]; if (status.intValue == 0) { //校驗成功,發放內容 //status code 0為成功 } else { //校驗失敗,不做處理或相應懲罰 //21000 App Store不能讀取你提供的JSON對象 //21002 receipt-data域的數據有問題 //21003 receipt無法通過驗證 //21004 提供的shared secret不匹配你賬號中的shared secret //21005 receipt服務器當前不可用 //21006 receipt合法,但是訂閱已過期。服務器接收到這個狀態碼時,receipt數據仍然會解碼并一起發送 //21007 receipt是Sandbox receipt,但卻發送至生產系統的驗證服務 //21008 receipt是生產receipt,但卻發送至Sandbox環境的驗證服務 } } - (void)requestFailed:(ASIHTTPRequest *)request { //出錯處理 } |
蘋果的返回值如下:
使用transaction.transactionReceipt取得的小票經二次驗證后返回值:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | { "receipt": { "original_purchase_date_pst":"2015-06-03 04:00:37 America/Los_Angeles", "purchase_date_ms":"1433329237329", "unique_identifier":"secret9f135e2cd8f7dda951a15c01cd2220c60b", "original_transaction_id":"1000000157783770", "bvrs":"2.6.0", "transaction_id":"1000000157783770", "quantity":"1", "unique_vendor_identifier":"SECRETCD-89AD-45C4-8937-359CCA9E8F36", "item_id":"SECRET509", "product_id":"com.your.iap.product.id", "purchase_date":"2015-06-03 11:00:37 Etc/GMT", "original_purchase_date":"2015-06-03 11:00:37 Etc/GMT", "purchase_date_pst":"2015-06-03 04:00:37 America/Los_Angeles", "bid":"com.your.app.bundle.id", "original_purchase_date_ms":"1433329237329" }, "status": 0 } |
使用[[NSBundle mainBundle] appStoreReceiptURL]取得的小票經二次驗證后返回值:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | { "status":0, "environment":"Sandbox", "receipt": { //in_app 中全部支付共有的信息(本App的IAP信息) "receipt_type":"ProductionSandbox", "adam_id":0, "app_item_id":0, "bundle_id":"com.netease.neteasemusic", "application_version":"2.9.0", "download_id":0, "version_external_identifier":0, "request_date":"2015-07-29 03:37:17 Etc/GMT", "request_date_ms":"1438141037628", "request_date_pst":"2015-07-28 20:37:17 America/Los_Angeles", "original_purchase_date":"2013-08-01 07:00:00 Etc/GMT", "original_purchase_date_ms":"1375340400000", "original_purchase_date_pst":"2013-08-01 00:00:00 America/Los_Angeles", "original_application_version":"1.0", "in_app": [ //每筆交易分別的信息 { "quantity":"1", "product_id":"com.netease.neteasemusictest.pack.clouddrive.2t12", "transaction_id":"1000000164784521", "original_transaction_id":"1000000164784521", "purchase_date":"2015-07-23 15:03:04 Etc/GMT", "purchase_date_ms":"1437663784000", "purchase_date_pst":"2015-07-23 08:03:04 America/Los_Angeles", "original_purchase_date":"2015-07-23 15:03:04 Etc/GMT", "original_purchase_date_ms":"1437663784000", "original_purchase_date_pst":"2015-07-23 08:03:04 America/Los_Angeles", "is_trial_period":"false" }, { "quantity":"1", "product_id":"com.netease.neteasemusictest.pack.clouddrive.1t12", "transaction_id":"1000000165136224", "original_transaction_id":"1000000165136224", "purchase_date":"2015-07-27 06:40:40 Etc/GMT", "purchase_date_ms":"1437979240000", "purchase_date_pst":"2015-07-26 23:40:40 America/Los_Angeles", "original_purchase_date":"2015-07-27 06:40:40 Etc/GMT", "original_purchase_date_ms":"1437979240000", "original_purchase_date_pst":"2015-07-26 23:40:40 America/Los_Angeles", "is_trial_period":"false" } ] } } |
純本地驗證
除了網絡驗證以外,蘋果提供了純粹的本地驗證方式:Validating Receipts Locally.
Receipt data 經過 App Store 證書簽名,所以第三方無法憑空生成能夠通過此法驗證的 receipt data。只要做好證書校驗,無需擔心用戶會偽造 receipt data。
在客戶端使用這種方式可以做到防止被通用破解方式破解,但并不能防止針對特定 App 的破解。
實際上,這種驗證方式是蘋果為服務端設計的。Receipt data 的格式遵守ASN.1格式,服務端安裝asn1c就可以解析 receipt data,并不需要純手寫一份解析代碼。只要服務端代碼和 asn1c 不出 bug,在服務端使用這種方式驗證就是安全的。
第三方網站驗證
有些第三方網站提供了經服務端的驗證服務。比如urbanairship. 但是我并沒有用過,所以不知道具體效果如何。畢竟第三方服務無法做到在用戶發起購買之前生成訂單記錄,與購買后驗證結果比對,所以我還是比較擔心第三方驗證服務的安全性的。而且雞國網絡連國外驗證服務器,你懂的。。
總之想要萬無一失,建議開發自己的驗證接口。
更多驗證相關問題,請參考Receipt Validation Programming Guide
大多數產品在驗證成功后,才是真正的發放內容、道具等。特別是充值后立即消費的虛擬貨幣基本都是這么處理的。
但是從接口來看, IAP 的設計者是想讓開發者在購買完成時發放內容、道具,在二次驗證失敗時以刪除內容、道具等方式來進行處罰。
6.服務端二次驗證后再發放數據中的安全問題
由于是和錢關系最緊密的功能,IAP安全性顯得無比重要。
客戶端數據安全
客戶端根據使用不同的獲取 receipt 的接口,需要做的事情也不同:
1. 客戶端使用transaction.transactionReceipt(或使用transaction.transactionReceipt + appStoreReceiptURL兩者):
如果要支持 iOS6,那么不得不使用transaction.transactionReceipt在 iOS6上讀取receipt,客戶端需要保證持久化邏輯:
在transaction完成后,和服務端的二次驗證完成前,要對receipt data做持久化; 若存在未上傳成功的 receipt ,需要開定時器重試上傳; 如果要做刪除持久化數據,刪除的時機應當是收到從服務端發回的二次驗證請求的響應時,確認服務端已和蘋果完成通信之后(服務端返回和蘋果連接失敗則不應刪除已保存的receiptData),若 IAP 交易不頻繁,可考慮不刪除持久化的 receipt data。
2. 客戶端僅使用appStoreReceiptURL獲取receipt
客戶端每次交易完成從 appStoreReceiptURL 讀取出 data 上傳給服務器。同時有上傳請求正在進行的話,持久化一個標記,表示有未完成的上傳,在全部上傳完成后將此標記置為上傳結束。如果標記存在,需要開定時器重試上傳。
服務端數據安全
服務端安全包含兩部分:防止已扣款卻購買無效;防止作弊。目前想到最圓的實踐是:
1. 在從客戶端上傳的 receipt data 中解析出的數據(包括本地解析或經蘋果服務器解析)中,從全部交易記錄中找到所有未被標記為已使用的交易記錄,作為集合A。
2. 枚舉當前用戶的全部待支付訂單,匹配集合A中的交易記錄中product id相同的項,并將此交易記錄的 transaction id 記錄下,標記為已使用;并將此訂單標記為已支付,并發放道具。
3. 線上環境僅在審核期間允許使用sandbox環境做二次驗證,防止上線后內部同事使用sandbox test user免費充值道具到線上環境。需注意審核期間必須開啟 sandbox 環境驗證,不然會被 rejected。
其他安全問題
7.切換線上/測試環境
需要在代碼里顯示聲明的環境,就只有二次驗證地址:
| 1 2 | #define SANDBOX_VERIFY_RECEIPT_URL [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"] #define APP_STORE_VERIFY_RECEIPT_URL [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"] |
而在Cocoa中調用蘋果接口時,開發證書、Beta測試證書、提交證書編譯出的App連接的都是Sandbox環境;只有上線后的App連接的是線上環境。
另外再次強調,除非少量必要的自己線上環境的測試需要連接蘋果的 Sandbox 驗證服務之外,自己服務端的二次驗證 API 應該嚴格做到自己的環境是線上環境,則連接蘋果的線上環境二次驗證接口。防止監守自盜的情況出現。
8.提交審核
如果是初次提交審核,IAP 商品要和第一個支持 IAP 的版本一起提交。審核期間要允許 sandbox 環境二次驗證。
后續新增的 IAP 商品則沒有此限制,可以隨時提交審核。
Over
總結
- 上一篇: css特殊符号编码大全
- 下一篇: 如何跨网络远程操作另一台计算机,如何远程