iOS 测试 App 收据验证

25

有很多关于如何使用沙盒测试账号进行应用内购买收据验证的例子。

但是对于付费应用本身的收据,该怎么办呢?我们如何在开发环境中获取应用程序收据?

我想要做两件事:

  • 防止没有购买应用的用户运行我们的应用程序。我见过一些应用程序检测到与iTune帐户连接的不拥有该应用程序的用户(它向用户显示未拥有该应用程序的警告,但不能阻止用户继续使用该应用程序)。

  • 将应用程序购买收据发送到我们的服务器。我们想知道他们何时购买我们的应用程序,购买的应用程序版本是什么。

4个回答

41
大部分答案可以在苹果的文档这里找到。但存在空缺,而且 Objective-C 代码使用了已不推荐使用的方法。
下面是展示如何获取应用收据并将其发送到应用商店进行验证的 Swift 3 代码。在保存所需数据之前,你应该向应用商店验证应用收据。请求应用商店验证的好处是它会响应带数据的 JSON 序列化,从中可以提取所需密钥的值。无需加密。
正如苹果在文档中所述,首选流程如下...
device -> your trusted server -> app store -> your trusted server -> device

当应用商店返回到您的服务器(假设成功),这就是您需要序列化和提取所需数据并按您希望的方式保存它的地方。请参见下面的JSON。您可以将结果和其他任何您想要的内容发送回应用程序。
在下面的validateAppReceipt()中,为了使其成为一个可工作的示例,它仅使用以下流程...
device -> app store -> device

为了使其与您的服务器配合使用,只需将validationURLString更改为指向您的服务器,并在requestDictionary中添加其他所需内容。
要在开发中测试此功能,您需要:
  • 确保您在itunesconnect中设置了沙盒用户
  • 在您的测试设备上退出iTunes和App Store
  • 在测试期间,当提示时,请使用您的沙箱用户
以下是代码。正常流程可以顺利进行。错误和失败点只会打印或被注释掉。根据您的需要处理这些问题。
此部分获取应用程序收据。如果没有收据(当进行测试时会出现),则请求应用商店进行刷新。
let receiptURL = Bundle.main.appStoreReceiptURL

func getAppReceipt() {
    guard let receiptURL = receiptURL else {  /* receiptURL is nil, it would be very weird to end up here */  return }
    do {
        let receipt = try Data(contentsOf: receiptURL)
        validateAppReceipt(receipt)
    } catch {
        // there is no app receipt, don't panic, ask apple to refresh it
        let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
        appReceiptRefreshRequest.delegate = self
        appReceiptRefreshRequest.start()
        // If all goes well control will land in the requestDidFinish() delegate method.
        // If something bad happens control will land in didFailWithError.
    }
}

func requestDidFinish(_ request: SKRequest) {
    // a fresh receipt should now be present at the url
    do {
        let receipt = try Data(contentsOf: receiptURL!) //force unwrap is safe here, control can't land here if receiptURL is nil
        validateAppReceipt(receipt)
    } catch {
        // still no receipt, possible but unlikely to occur since this is the "success" delegate method
    }
}

func request(_ request: SKRequest, didFailWithError error: Error) {
    print("app receipt refresh request did fail with error: \(error)")
    // for some clues see here: https://samritchie.net/2015/01/29/the-operation-couldnt-be-completed-sserrordomain-error-100/
}

这部分验证应用程序收据。这不是本地验证。请参考注释中的备注1和备注2。

func validateAppReceipt(_ receipt: Data) {

    /*  Note 1: This is not local validation, the app receipt is sent to the app store for validation as explained here:
            https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1
        Note 2: Refer to the url above. For good reasons apple recommends receipt validation follow this flow:
            device -> your trusted server -> app store -> your trusted server -> device
        In order to be a working example the validation url in this code simply points to the app store's sandbox servers.
        Depending on how you set up the request on your server you may be able to simply change the 
        structure of requestDictionary and the contents of validationURLString.
    */
    let base64encodedReceipt = receipt.base64EncodedString()
    let requestDictionary = ["receipt-data":base64encodedReceipt]
    guard JSONSerialization.isValidJSONObject(requestDictionary) else {  print("requestDictionary is not valid JSON");  return }
    do {
        let requestData = try JSONSerialization.data(withJSONObject: requestDictionary)
        let validationURLString = "https://sandbox.itunes.apple.com/verifyReceipt"  // this works but as noted above it's best to use your own trusted server
        guard let validationURL = URL(string: validationURLString) else { print("the validation url could not be created, unlikely error"); return }
        let session = URLSession(configuration: URLSessionConfiguration.default)
        var request = URLRequest(url: validationURL)
        request.httpMethod = "POST"
        request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
        let task = session.uploadTask(with: request, from: requestData) { (data, response, error) in
            if let data = data , error == nil {
                do {
                    let appReceiptJSON = try JSONSerialization.jsonObject(with: data)
                    print("success. here is the json representation of the app receipt: \(appReceiptJSON)")
                    // if you are using your server this will be a json representation of whatever your server provided
                } catch let error as NSError {
                    print("json serialization failed with error: \(error)")
                }
            } else {
                print("the upload task returned an error: \(error)")
            }
        }
        task.resume()
    } catch let error as NSError {
        print("json serialization failed with error: \(error)")
    }
}

你应该最终得到类似这样的结果。在你的情况下,这就是你在服务器上使用的内容。
{
    environment = Sandbox;
    receipt =     {
        "adam_id" = 0;
        "app_item_id" = 0;
        "application_version" = "0";  // for me this was showing the build number rather than the app version, at least in testing
        "bundle_id" = "com.yourdomain.yourappname";  // your app's actual bundle id
        "download_id" = 0;
        "in_app" =         (
        );
        "original_application_version" = "1.0"; // this will always return 1.0 when testing, the real thing in production.
        "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";
        "receipt_creation_date" = "2016-09-21 18:46:39 Etc/GMT";
        "receipt_creation_date_ms" = 1474483599000;
        "receipt_creation_date_pst" = "2016-09-21 11:46:39 America/Los_Angeles";
        "receipt_type" = ProductionSandbox;
        "request_date" = "2016-09-22 18:37:41 Etc/GMT";
        "request_date_ms" = 1474569461861;
        "request_date_pst" = "2016-09-22 11:37:41 America/Los_Angeles";
        "version_external_identifier" = 0;
    };
    status = 0;
}

2
谢谢您提供的示例!我已经寻找了相当长的时间,这个示例运行得非常完美。 - EricH206
1
谢谢这个。非常非常有帮助。然而,没有经验的人(包括我自己)应该注意,如果你想将收据数据传递到自己的php脚本上的受信任的服务器,那么在将其传递给苹果的验证之前,base64编码的数据必须被清理。我仍在努力找出如何最好地做到这一点。对于上面的示例,这不是一个问题,因为它直接将请求字典传递给https://sandbox.itunes.apple.com/verifyReceipt - Peter Wiley
在生产环境中,请使用此网址:https://buy.itunes.apple.com/verifyReceipt - Murray Sagal
@MurraySagal。谢谢。实际版本号的笔记很好。我的应用商店版本只有主/次版本,例如1.12,而构建号则是类似于34的东西。所以你的意思是我会得到34吗? - Augie
@Augie 是的。你会得到“34”。 - Murray Sagal
显示剩余10条评论

6
我假设您知道如何执行应用内购买。
交易完成后,我们需要验证收据。
- (void)completeTransaction:(SKPaymentTransaction *)transaction 
{
    NSLog(@"completeTransaction...");
    
    [appDelegate setLoadingText:VALIDATING_RECEIPT_MSG];
    [self validateReceiptForTransaction];
}

产品购买成功后,需要进行验证。服务器会为我们完成此操作,我们只需传递由苹果服务器返回的购买凭证数据即可。

-(void)validateReceiptForTransaction
{
    /* Load the receipt from the app bundle. */
    
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    
    if (!receipt) { 
        /* No local receipt -- handle the error. */
    }
    
    /* ... Send the receipt data to your server ... */
    
    NSData *receipt; // Sent to the server by the device
    
    /* Create the JSON object that describes the request */
    
    NSError *error;
    
    NSDictionary *requestContents = @{ @"receipt-data": [receipt base64EncodedStringWithOptions:0] };
    
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                          options:0
                                                            error:&error];
    
    if (!requestData) { 
        /* ... Handle error ... */ 
    }
    
    // Create a POST request with the receipt data.
    
    NSURL *storeURL = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"];
    
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];
    
    /* Make a connection to the iTunes Store on a background queue. */
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                               
                               if (connectionError) {
                                   /* ... Handle error ... */
                               } 
                               else {
                                   NSError *error;
                                   NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
                                   
                                   if (!jsonResponse) { 
                                       /* ... Handle error ...*/ 
                                   }
                                   
                                   /* ... Send a response back to the device ... */
                               }
                           }];
}

响应的有效载荷是一个JSON对象,包含以下键和值:

状态:

如果收据有效,则为0,否则为下面提到的错误代码之一:

enter image description here

对于 iOS 6 风格的交易收据,状态码反映了特定交易收据的状态。

对于 iOS 7 风格的应用程序收据,状态码反映了整个应用程序收据的状态。例如,如果您发送一个包含已过期订阅的有效应用程序收据,则响应为 0,因为整个收据是有效的。

收据:

发送进行验证的收据的 JSON 表示形式。

请记住:


编辑 1

transactionReceipt已被弃用:自iOS 7.0起首次被弃用

if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) {
    // iOS 6.1 or earlier.
    // Use SKPaymentTransaction's transactionReceipt.

} else {
    // iOS 7 or later.

    NSURL *receiptFileURL = nil;
    NSBundle *bundle = [NSBundle mainBundle];
    if ([bundle respondsToSelector:@selector(appStoreReceiptURL)]) {

        // Get the transaction receipt file path location in the app bundle.
        receiptFileURL = [bundle appStoreReceiptURL];

        // Read in the contents of the transaction file.

    } else {
        /* Fall back to deprecated transaction receipt,
           which is still available in iOS 7.
           Use SKPaymentTransaction's transactionReceipt. */
    }

}

2
所以即使在开发版本中(尚未发布给公众),我仍然可以获得应用程序收据吗?(目前我们仍处于调查过程中,因此我没有测试环境) - King Chan
是的,在沙盒环境中您仍将获得验证收据。基本上,您需要在iTunes Connect中设置一个测试用户帐户。然后,您只需使用该凭据进行购买,就会收到没有财务交易的收据。 - NSPratik
2
这个链接讨论的是测试应用内购买,但我想谈论的是应用(付费应用)本身的收据? - King Chan
你的意思是开发构建吗?因为有应用程序收据字段 https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW2 - King Chan
不,您可以滚动到顶部,它会显示“应用程序收据字段”,包含字段:Bundle Identifier、App Version、Opaque Value、SHA-1 Hash、In-App Purchase Receipt等。您还可以在左侧看到应用程序收据部分和应用内收据部分。 - King Chan
显示剩余5条评论

2

iOS 13可用

以下是在设备上验证收据而无需任何服务器代码的步骤:

在验证收据之前,您需要密码。这将是共享的密钥。

如何生成它:

进入“合同、税务和银行”并单击iOS付费应用程序合同上的“请求”,然后接受合同。

访问此链接

https://appstoreconnect.apple.com

1:- 点击功能

2:- 点击应用内购买并创建您的订阅套餐

3:- 成功创建订阅后,点击应用程序特定共享秘密

4:- 生成应用程序特定共享秘密


更新的代码以验证应用内订阅收据:

-(void) verifyReceipt
{
/* Load the receipt from the app bundle. */

NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

if (!receipt) {
    /* No local receipt -- handle the error. */
}

/* Create the JSON object that describes the request */
NSError *error;

/* reciept data and password to be sent, password would be the Shared Secret Key from Apple Developer account for given app. */
NSDictionary *requestContents = @{
                                  @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                                 ,@"password": @"2008687bb49145445457ff2b25e9bff3"};

NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                      options:0
                                                        error:&error];

if (!requestData) {
    /* ... Handle error ... */
}

// Create a POST request with the receipt data.
NSURL *storeURL = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];

NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];

/* Make a connection to the iTunes Store on a background queue. */
//NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:storeRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    // handle request error
    if (error) {
        //completion(nil, error);
        return;
    } else {
        NSError *error;
        NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];

        if (!jsonResponse) {
            /* ... Handle error ...*/
        }

        /* ... Send a response back to the device ... */
    }
}];
[dataTask resume];
}

希望这能有所帮助。谢谢。

0

如果您想在应用内测试,请进入沙盒环境进行收据验证,并请注意,在沙盒中续订间隔为:

1周3分钟 1个月5分钟 2个月10分钟 3个月15分钟 6个月30分钟 1年1小时

最好的方法是通过与苹果服务器通信来验证收据。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接