1. 适用状况
想使用In-App Purchase(如下简称IAP)完成App内付费前,先肯定需求是否是能用这个方案来知足。
除了IAP之外,还有支付宝SDK、信用卡等其余方式完成软件内付费。html
在苹果制定的游戏规则中,全部在App内提供的服务须要付费时,都应当使用IAP,好比软件功能、游戏道具;全部在App外提供的服务须要付费时,都应使用其余支付方式,好比Uber的信用卡,淘宝、快的打车的支付宝SDK等。ios
在IAP里,能够出售:git
- 数字内容:好比杂志、图片、游戏关卡解锁、相机付费滤镜等;
- 软件功能:如各类扩展features;
- 一次性服务:好比一次语音通话等。注意是「一次性」服务,不是一次「性服务」。
在IAP里,不能出售:github
- 现实世界的商品或服务:好比刚才提到的一次「性服务」。严格遵照此方案有个好处:IAP 若是被破解,用户没法获得大量实物,开发商也不会有很大经济损失。非要作的话想绕过也是能够的:用 IAP 购买代币,审核经过后用代币购买实物。
- 其余the App Review GuideLines中规定的不容许的内容:好比刚才提到的一次「性服务」。
顺便说下,有次大网易的同事分享时提到:使用兑换码兑换App内服务是一条高压线。像Uber和Amazon里容许有码,是由于他们的码是用在现实世界的产品或服务上的。objective-c
若是你肯定内购需求符合IAP的使用要求,能够继续往下读了。安全
2. 购买及发放虚拟产品流程
官方给出的流程图是这样的:服务器
- 获取内购列表(从App内读取或从本身服务器读取)
- App Store请求可用的内购列表
- 向用户展现内购列表
- 用户选择了内购列表,再发个购买请求
- 收到购买完成的回掉
- 发放虚拟产品
3.虚拟产品
虚拟产品的分类
虚拟产品分为如下几种类型:网络
- 消耗品(Consumable products):好比游戏内金币等。
- 不可消耗品(Non-consumable products):简单来讲就是一次购买,终身可用(用户可随时从App Store restore)。
- 自动更新订阅品(Auto-renewable subscriptions):和不可消耗品的不一样点是有失效时间。好比一全年的付费周刊。在这种模式下,开发者按期投递内容,用户在订阅期内随时能够访问这些内容。订阅快要过时时,系统将自动更新订阅(若是用户赞成)。
- 非自动更新订阅品(Non-renewable subscriptions):通常使用场景是从用户从IAP购买后,购买信息存放在本身的开发者服务器上。失效日期/可用是由开发者服务器自行控制的,而非由App Store控制,这一点与自动更新订阅品有差别。
- 免费订阅品(Free subscriptions):在Newsstand中放置免费订阅的一种方式。免费订阅永不过时。只能用于Newsstand-enabled apps。
类型二、三、5都是以Apple ID为粒度的。好比小张有三个iPad,有一个Apple ID购买了不可消耗品,则三个iPad上均可以使用。
类型一、4通常来讲则是现买现用。若是开发者本身想作更多控制,通常选4。并发
几种产品的区别以下(表格懒得翻译了):app
Table 1-1 Comparison of product types
Product type | Non-consumable | Consumable |
---|---|---|
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
Subscription type | Auto-renewable | Non-renewing | Free |
---|---|---|---|
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交互
填写银行卡与纳税信息
- 登陆iTunes Connect
- 点击Agreements, Tax, and Banking,填写Contact Info, Bank Info, Tax Info。若是填过就不用再填了。刚刚填写完成后,各类信息正在审核,以下图:
即便信息正在审核,沙箱环境下也是能够访问IAP服务的,并不须要等审核完成才能测试。
新建虚拟产品
- 登陆iTunes Connect
- 点击My Apps
- 进入想使用IAP的App详情
- 选择In-App Purchases
- 点击Create New
- 选择IAP虚拟产品类型。注意虚拟产品一旦新建,类型没法修改。
- 填入Internal Name。只能在iTunes Connect中看到这个名字。不会出如今App Store中。最长255字节。
- 填入Product ID。每件产品有一个单独的Product ID,Product ID用于从App Store获取价格信息,以及付费时标识是哪一种产品被购买了。例如com.163.neteasemusic.skin.dog。这个新建以后也是不能修改的。
- 而后是设置价格。Cleared For Sale选为YES是虚拟产品被审核经过自动上架。NO是手动上架。Price Tier则是价格。
- 多语言描述,这个是给用户看的。
- 是否要在iTunes托管可购买内容,这个后面再说
- 填入review notes和review用的截图。
- 保存。
- 保存后除了类型和Product ID均可以修改
新建完,不用等待苹果审核就能够在沙箱环境使用了。
新建测试账号
- 登陆iTunes Connect
- 点击Users and Roles
- 点击Sandbox Testers
- 添加Tester。添加后在测试机上用tester账号登陆app store
附:在苹果托管不可消耗品(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. 代码里该作的事情
获取产品列表
- 首先读取出App中内嵌的或是服务端中的Product IDs。
- 使用SKProductRequest向苹果服务器验证哪些Product IDs是可用的。注意不要在[Class load]里发送请求,必定要等到App didFinishLauching以后再发,否则没法接到请求返回。核心代码以下:
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。
其余安全问题
- 尽可能使用纯 HTTPS 接口上传 receipt,并严格校验 SSL 证书,防止中间人攻击。
- 越狱后有木马会盗取用户的支付,若是必要,须要提醒用户越狱风险。
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