iOS收据验证 - 自动续订订阅

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS收据验证 - 自动续订订阅相关的知识,希望对你有一定的参考价值。

我在验证自动续订IAP的收据时遇到问题。以下是我验证订阅的代码摘要。我正在使用Parse作为后端,并且还包含一些代码。在开发模式中,一切都运行良好,没有问题。它存在于App Store中,一旦应用程序尝试验证,我的所有TestFlight测试人员和一些真实用户都会遇到崩溃。当我尝试将数据保存回Parse时,我发现崩溃来自函数的最后,它告诉我我保存的密钥为空。 (base64infoexpirationDate

在我的设备上,我有一个沙箱购买收据,我收到了来自Apple的21007回复,试图验证实时URL。当我切换到沙盒网址时,它每次都会验证并运行。

此外,我知道以这种方式验证购买是不安全的,我通过我的服务器执行验证,所以没有问题。

我想知道我是否错过了一步,如果有什么我应该采取不同的做法?

伪代码:

Get the receipt from NSBundle
Get the receipt from Parse database
If neither of them exist:
    end the function
else:
    if Parse receipt exists:
        use it, but first just check the expirationDate stored in Parse
    else:
        use NSBundle receipt for validation, 
    If expired based on date from Parse:
        build request to send to Apple (my server in production)
        Get JSON response, and perform switch statement for response codes
    Check for errors or expiration
    Save new data to Parse // <-- Cause of the crash is here because the keys are null for some users

这是一段实际代码:

PFUser *user = [PFUser currentUser];

//Load the receipt from the app bundle
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

//Load the receipt from Parse, already encoded
NSString *saved64 = user[@"base64"];

//Check for at least one instance of a receipt... Either Parse or appBundle
if (!receipt && saved64.length == 0) {
//if (!receipt) {
    //No receipt
    NSLog(@"No Receipt");
    return;
}

//Base 64 encode appBundle receipt
NSString *receipt64 = [receipt base64EncodedStringWithOptions:0];

//String to hold base64 receipt (either from Parse or appBundle)
NSString *temp64;

//See if Parse base64 exists
if (saved64.length == 0) {
    //Not a receipt in Parse yet, use appBundle
    NSLog(@"Using appBundle receipt.");
    temp64 = receipt64;
} else {
    //Receipt in Parse, use it
    NSLog(@"Using Parse receipt.");
    temp64 = saved64;

    //Check expiration date stored in Parse
    NSDate *parseExpDate = user[@"expirationDate"];
    if ([[self todayGMT] compare:parseExpDate] == NSOrderedAscending) {
        //Active based on Parse, no need to validate...
        NSLog(@"Active based on Parse receipt... No need to validate!");
        return;
    }
}

    //Base 64 encode appBundle receipt
    NSString *receipt64 = [receipt base64EncodedStringWithOptions:0];


    //Create the request
    NSString *sharedSecret = @"[shared-secret]";
    NSError *error;
    //Request with receipt data and shared secret from iTunesConnect
    NSDictionary *requestContents = @{@"receipt-data":receipt64, @"password": sharedSecret};
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
    if (!requestData) {
        //Handle error
        NSLog(@"Error: %@", error);

        return;
    }

    //Create a POST request with the receipt data.
    NSURL *storeURL = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"];                      
    //NSURL *sandboxURL = [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
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (connectionError) {
            //Error
        } else {
            //Success
            NSError *error;
            NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
            if (!jsonResponse) {
                //Error
                NSLog(@"Error at !jsonResponse: %@", error);
                return;
            }

            NSString *base64 = jsonResponse[@"latest_receipt"];
            NSArray *info = jsonResponse[@"latest_receipt_info"];
            BOOL isPro;

            //Switch statement for subscription status
            switch ([jsonResponse[@"status"] intValue]) {
                case 21002: {
                    //The data in the receipt-data property was malformed or missing.
                    NSLog(@"21002 : The data in the receipt-data property was malformed or missing.");
                    isPro = NO;
                    break;
                }
                case 21003: {
                    //The receipt could not be authenticated.
                    NSLog(@"21003 : The receipt could not be authenticated.");
                    isPro = NO;
                    break;
                }
                case 21004: {
                    //The shared secret you provided does not match the shared secret on file for your account.
                    NSLog(@"21004 : The shared secret you provided does not match the shared secret on file for your account.");
                    isPro = NO;
                    break;
                }
                case 21005: {
                    //The receipt server is not currently available.
                    NSLog(@"21005 : The receipt server is not currently available.");
                    isPro = NO;
                    break;
                }
                case 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.
                    NSLog(@"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.");
                    isPro = NO;
                    break;
                }
                case 21007: {
                    //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.
                    NSLog(@"21007 : This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead..");
                    isPro = NO;
                    break;
                }
                case 21008: {
                    //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.
                    NSLog(@"21008 : This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead..");
                    isPro = NO;
                    break;
                }
                case 0: {
                    //Valid and active
                    NSLog(@"0 : Valid and active subscription.");
                    isPro = YES;
                    break;
                }
                default: {
                    isPro = NO;
                    break;
                }
            }

            //Set user info to database (Parse)
            user[@"base64"] = base64;
            user[@"info"] = info;
            user[@"expirationDate"] = expirationDate;
            user[@"isPro"] = [NSNumber numberWithBool:isPro];
            [user saveEventually];
        }
    }];
答案

沙盒网址 - https://sandbox.itunes.apple.com/verifyReceipt

Sandbox URL仅在开发模式下使用开发人员证书,因此它从服务器获取响应。

实时网址 - https://buy.itunes.apple.com/verifyReceipt

实时URL仅在分发模式下使用分发证书,因此它在开发模式下无效并且无法返回响应。

因此,如果要将实时URL与调试模式一起使用,则应该处理异常。

如果您使用带有可选绑定的Swift,则可以在不崩溃的情况下进行解析。

以上是关于iOS收据验证 - 自动续订订阅的主要内容,如果未能解决你的问题,请参考以下文章

iOS 自动更新订阅:收据验证流程

从验证收据中检查自动续订订阅到期日期

验证服务器上的自动续订订阅收据

应用购买iphone中的自动续订验证

iOS应用程序购买 - 自动续订订阅

iOS 7 自动续订订阅到期