本文介绍了一个完整的解决方案,可以在iOS 7上验证应用内收据和捆绑收据的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

限时删除!!

我已经阅读了很多文档和代码,理论上这些文档和代码将验证应用内和/或捆绑收据。



鉴于我对SSL,证书,加密等知识几乎为零,我读过的所有解释都是,我发现很难理解。



他们说解释是不完整的,因为每个人都有找出如何做到这一点,或者黑客可以轻松创建一个能够识别和识别模式并修补应用程序的破解程序应用程序。好的,我同意这一点。我认为他们可以完全解释如何做,并发出警告说修改此方法,修改此其他方法,混淆此变量,更改此名称和那个,等等。



可以帮助一些好心灵解释如何在iOS 7上进行本地验证,捆绑收据和应用内购买收据,因为我已经五年了从上到下,显然是旧的(好吧,做到3)?



谢谢!!!






如果你有一个版本正在处理你的应用程序而你的担心是黑客会看到你是如何做到的,那么只需在发布之前更改你的敏感方法。混淆字符串,改变行的顺序,改变循环的方式(从使用到阻止枚举,反之亦然)等等。显然,每个使用可能在此处发布的代码的人都必须做同样的事情,而不是冒着被黑客攻击的风险。

解决方案

以下是我在应用内购买库中解决此问题的演练。我将解释如何验证交易,包括验证整个收据。



一目了然



获取收货并验证交易。如果失败,请刷新收据并重试。这使得验证过程异步,因为刷新收据是异步的。



来自:

  RMAppReceipt * receipt = [ RMAppReceipt bundleReceipt]; 
const BOOL Verified = [self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:nil]; // failureBlock故意为零。见下文。
if(已验证)返回;

// Apple建议在iOS
验证失败时刷新收据[[RMStore defaultStore] refreshReceiptOnSuccess:^ {
RMAppReceipt * receipt = [RMAppReceipt bundleReceipt];
[self verifyTransaction:transaction inReceipt:收到成功:successBlock failure:failureBlock];
}失败:^(NSError *错误){
[self failWithBlock:failureBlock error:error];
}];



获取收据数据



收据在 [[NSBundle mainBundle] appStoreReceiptURL] 中,实际上是一个PCKS7容器。我吮吸密码学所以我用OpenSSL打开这个容器。其他人显然是用完成的。



将OpenSSL添加到您的项目并非易事。 应该有所帮助。



如果您选择使用OpenSSL打开PKCS7容器,您的代码可能如下所示。来自:

  +(NSData *)dataFromPKCS7Path:(NSString *)path 
{
const char * cpath = [[path stringByStandardizingPath] fileSystemRepresentation] ;
FILE * fp = fopen(cpath,rb);
如果(!fp)返回nil;

PKCS7 * p7 = d2i_PKCS7_fp(fp,NULL);
fclose(fp);

如果(!p7)返回nil;

NSData *数据;
NSURL * certificateURL = [[NSBundle mainBundle] URLForResource:@AppleIncRootCertificatewithExtension:@cer];
NSData * certificateData = [NSData dataWithContentsOfURL:certificateURL];
if([self verifyPKCS7:p7 withCertificateData:certificateData])
{
struct pkcs7_st * contents = p7-> d.sign-> contents;
if(PKCS7_type_is_data(contents))
{
ASN1_OCTET_STRING * octets = contents-> d.data;
data = [NSData dataWithBytes:octets-> data length:octets-> length];
}
}
PKCS7_free(p7);
返回数据;
}

我们稍后会详细介绍验证。



获取收据字段



收据以ASN1格式表示。它包含一般信息,一些用于验证目的的字段(我们稍后会介绍)以及每个适用的应用内购买的具体信息。



同样,OpenSSL在阅读ASN1时也能解决问题。从,使用一些辅助方法:

  NSMutableArray * purchases = [NSMutableArray array]; 
[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData * data,int type){
const uint8_t * s = data.bytes;
const NSUInteger length = data.length;
开关(类型)
{
case RMAppReceiptASN1TypeBundleIdentifier:
_bundleIdentifierData = data;
_bundleIdentifier = RMASN1ReadUTF8String(& s,length);
休息;
case RMAppReceiptASN1TypeAppVersion:
_appVersion = RMASN1ReadUTF8String(& s,length);
休息;
case RMAppReceiptASN1TypeOpaqueValue:
_opaqueValue = data;
休息;
case RMAppReceiptASN1TypeHash:
_hash = data;
休息;
case RMAppReceiptASN1TypeInAppPurchaseReceipt:
{
RMAppReceiptIAP * purchase = [[RMAppReceiptIAP alloc] initWithASN1Data:data];
[购买addObject:购买];
休息;
}
case RMAppReceiptASN1TypeOriginalAppVersion:
_originalAppVersion = RMASN1ReadUTF8String(& s,length);
休息;
case RMAppReceiptASN1TypeExpirationDate:
{
NSString * string = RMASN1ReadIA5SString(& s,length);
_expirationDate = [RMAppReceipt formatRFC3339String:string];
休息;
}
}
}];
_inAppPurchases =购买;



获取应用内购买



每个应用内购买也在ASN1中。解析它与解析一般收据信息非常相似。



来自,使用相同的帮助方法:

  [RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData * data,int type){
const uint8_t * p = data.bytes;
const NSUInteger length = data.length;
开关(类型)
{
case RMAppReceiptASN1TypeQuantity:
_quantity = RMASN1ReadInteger(& p,length);
休息;
case RMAppReceiptASN1TypeProductIdentifier:
_productIdentifier = RMASN1ReadUTF8String(& p,length);
休息;
case RMAppReceiptASN1TypeTransactionIdentifier:
_transactionIdentifier = RMASN1ReadUTF8String(& p,length);
休息;
case RMAppReceiptASN1TypePurchaseDate:
{
NSString * string = RMASN1ReadIA5SString(& p,length);
_purchaseDate = [RMAppReceipt formatRFC3339String:string];
休息;
}
case RMAppReceiptASN1TypeOriginalTransactionIdentifier:
_originalTransactionIdentifier = RMASN1ReadUTF8String(& p,length);
休息;
case RMAppReceiptASN1TypeOriginalPurchaseDate:
{
NSString * string = RMASN1ReadIA5SString(& p,length);
_originalPurchaseDate = [RMAppReceipt formatRFC3339String:string];
休息;
}
case RMAppReceiptASN1TypeSubscriptionExpirationDate:
{
NSString * string = RMASN1ReadIA5SString(& p,length);
_subscriptionExpirationDate = [RMAppReceipt formatRFC3339String:string];
休息;
}
case RMAppReceiptASN1TypeWebOrderLineItemID:
_webOrderLineItemID = RMASN1ReadInteger(& p,length);
休息;
case RMAppReceiptASN1TypeCancellationDate:
{
NSString * string = RMASN1ReadIA5SString(& p,length);
_cancellationDate = [RMAppReceipt formatRFC3339String:string];
休息;
}
}
}];

应该注意的是,某些应用内购买,例如消耗品和不可续订的订阅,在收据中只出现一次。您应该在购买后立即验证这些(同样,RMStore会帮助您)。



验证一目了然





下面是我们在开始时回调的方法。来自:

   - (BOOL)verifyTransaction:(SKPaymentTransaction *)交易
inReceipt:(RMAppReceipt *)收货
成功:(无效(^ )())successBlock
失败:(void(^)(NSError * error))failureBlock
{
const BOOL receiptVerified = [self verifyAppReceipt:receipt];
if(!receiptVerified)
{
[self failWithBlock:failureBlock message:NSLocalizedString(@应用收据验证失败,@)];
返回NO;
}
SKPayment * payment = transaction.payment;
const BOOL transactionVerified = [收货containsInAppPurchaseOfProductIdentifier:payment.productIdentifier];
if(!transactionVerified)
{
[self failWithBlock:failureBlock message:NSLocalizedString(@应用收据不包含给定产品,@)];
返回NO;
}
if(successBlock)
{
successBlock();
}
返回YES;
}



验证收据



验证收据本身归结为:


  1. 检查收据是否有效PKCS7和ASN1。我们已经隐含地这样做了。

  2. 验证收据是否由Apple签署。这是在解析收据之前完成的,详情如下。

  3. 检查收据中包含的包标识符是否与您的包标识符相对应。您应该对您的包标识符进行硬编码,因为修改您的应用包并使用其他收据似乎并不困难。

  4. 检查收据中包含的应用版本是否与您的应用版本标识符。您应该对应用版本进行硬编码,原因与上述相同。

  5. 检查收据哈希,确保收据与当前设备相对应。

高级代码中的5个步骤,来自:

   - (BOOL)verifyAppReceipt:(RMAppReceipt *)收据
{
//步骤1&解析收据时完成2个
如果(!收据)返回NO;

//步骤3
if(![receipt.bundleIdentifier isEqualToString:self.bundleIdentifier])return NO;

//步骤4
如果(![receipt.appVersion isEqualToString:self.bundleVersion])返回NO;

//步骤5
if(![receipt verifyReceiptHash])返回NO;

返回YES;
}

让我们深入了解第2步和第5步。



验证收据签名



当我们提取数据时,我们浏览了收据签名验证。收据使用Apple Inc.根证书签署,可从下载。以下代码将PKCS7容器和根证书作为数据并检查它们是否匹配:

  +(BOOL)verifyPKCS7 :( PKCS7 *)容器withCertificateData:(NSData *)certificateData 
{//基于:https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref / doc / uid / TP40010573-CH1-SW17
static int verified = 1;
int result = 0;
OpenSSL_add_all_digests(); // PKCS7_verify需要工作
X509_STORE * store = X509_STORE_new();
if(store)
{
const uint8_t * certificateBytes =(uint8_t *)(certificateData.bytes);
X509 * certificate = d2i_X509(NULL,& certificateBytes,(long)certificateData.length);
if(certificate)
{
X509_STORE_add_cert(商店,证书);

BIO * payload = BIO_new(BIO_s_mem());
result = PKCS7_verify(container,NULL,store,NULL,payload,0);
BIO_free(有效载荷);

X509_free(证书);
}
}
X509_STORE_free(商店);
EVP_cleanup(); //根据http://www.openssl.org/docs/crypto/OpenSSL_add_all_algorithms.html余额OpenSSL_add_all_digests()

返回结果==已验证;
}

这是在解析收据之前的开头做的。



验证收据哈希



收据中包含的哈希值是设备ID的SHA1,包括一些不透明值在收据和包ID中。



这是验证iOS上的收据哈希的方法。来自:

   - (BOOL)verifyReceiptHash 
{
// TODO:在Mac中获取uuid是不同的。请参阅:https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
NSUUID * uuid = [ [UIDevice currentDevice] identifierForVendor];
unsigned char uuidBytes [16];
[uuid getUUIDBytes:uuidBytes];

//订单取自:https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573- CH1-SW5
NSMutableData * data = [NSMutableData data];
[data appendBytes:uuidBytes length:sizeof(uuidBytes)];
[data appendData:self.opaqueValue];
[data appendData:self.bundleIdentifierData];

NSMutableData * expectedHash = [NSMutableData dataWithLength:SHA_DIGEST_LENGTH];
SHA1(data.bytes,data.length,expectedHash.mutableBytes);

return [expectedHash isEqualToData:self.hash];
}

这就是它的要点。我可能会在这里或那里遗漏一些东西,所以我稍后可能会回到这篇文章。无论如何,我建议浏览完整的代码以获取更多详细信息。


I have read a lot of docs and code that in theory will validate an in-app and/or bundle receipt.

Given that my knowledge of SSL, certificates, encryption, etc., is nearly zero, all of the explanations I have read, like this promising one, I have found difficult to understand.

They say the explanations are incomplete because every person has to figure out how to do it, or the hackers will have an easy job creating a cracker app that can recognize and identify patterns and patch the application. OK, I agree with this up to a certain point. I think they could explain completely how to do it and put a warning saying "modify this method", "modify this other method", "obfuscate this variable", "change the name of this and that", etc.

Can some good soul out there be kind enough to explain how to LOCALLY validate, bundle receipts and in-app purchase receipts on iOS 7 as I am five years old (ok, make it 3), from top to bottom, clearly?

Thanks!!!


If you have a version working on your apps and your concerns are that hackers will see how you did it, simply change your sensitive methods before publishing here. Obfuscate strings, change the order of lines, change the way you do loops (from using for to block enumeration and vice-versa) and things like that. Obviously, every person that uses the code that may be posted here, has to do the same thing, not to risk being easily hacked.

解决方案

Here's a walkthrough of how I solved this in my in-app purchase library RMStore. I will explain how to verify a transaction, which includes verifying the whole receipt.

At a glance

Get the receipt and verify the transaction. If it fails, refresh the receipt and try again. This makes the verification process asynchronous as refreshing the receipt is asynchronous.

From RMStoreAppReceiptVerifier:

RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
const BOOL verified = [self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:nil]; // failureBlock is nil intentionally. See below.
if (verified) return;

// Apple recommends to refresh the receipt if validation fails on iOS
[[RMStore defaultStore] refreshReceiptOnSuccess:^{
    RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
    [self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:failureBlock];
} failure:^(NSError *error) {
    [self failWithBlock:failureBlock error:error];
}];

Getting the receipt data

The receipt is in [[NSBundle mainBundle] appStoreReceiptURL] and is actually a PCKS7 container. I suck at cryptography so I used OpenSSL to open this container. Others apparently have done it purely with system frameworks.

Adding OpenSSL to your project is not trivial. The RMStore wiki should help.

If you opt to use OpenSSL to open the PKCS7 container, your code could look like this. From RMAppReceipt:

+ (NSData*)dataFromPKCS7Path:(NSString*)path
{
    const char *cpath = [[path stringByStandardizingPath] fileSystemRepresentation];
    FILE *fp = fopen(cpath, "rb");
    if (!fp) return nil;

    PKCS7 *p7 = d2i_PKCS7_fp(fp, NULL);
    fclose(fp);

    if (!p7) return nil;

    NSData *data;
    NSURL *certificateURL = [[NSBundle mainBundle] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"];
    NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL];
    if ([self verifyPKCS7:p7 withCertificateData:certificateData])
    {
        struct pkcs7_st *contents = p7->d.sign->contents;
        if (PKCS7_type_is_data(contents))
        {
            ASN1_OCTET_STRING *octets = contents->d.data;
            data = [NSData dataWithBytes:octets->data length:octets->length];
        }
    }
    PKCS7_free(p7);
    return data;
}

We'll get into the details of the verification later.

Getting the receipt fields

The receipt is expressed in ASN1 format. It contains general information, some fields for verification purposes (we'll come to that later) and specific information of each applicable in-app purchase.

Again, OpenSSL comes to the rescue when it comes to reading ASN1. From RMAppReceipt, using a few helper methods:

NSMutableArray *purchases = [NSMutableArray array];
[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) {
    const uint8_t *s = data.bytes;
    const NSUInteger length = data.length;
    switch (type)
    {
        case RMAppReceiptASN1TypeBundleIdentifier:
            _bundleIdentifierData = data;
            _bundleIdentifier = RMASN1ReadUTF8String(&s, length);
            break;
        case RMAppReceiptASN1TypeAppVersion:
            _appVersion = RMASN1ReadUTF8String(&s, length);
            break;
        case RMAppReceiptASN1TypeOpaqueValue:
            _opaqueValue = data;
            break;
        case RMAppReceiptASN1TypeHash:
            _hash = data;
            break;
        case RMAppReceiptASN1TypeInAppPurchaseReceipt:
        {
            RMAppReceiptIAP *purchase = [[RMAppReceiptIAP alloc] initWithASN1Data:data];
            [purchases addObject:purchase];
            break;
        }
        case RMAppReceiptASN1TypeOriginalAppVersion:
            _originalAppVersion = RMASN1ReadUTF8String(&s, length);
            break;
        case RMAppReceiptASN1TypeExpirationDate:
        {
            NSString *string = RMASN1ReadIA5SString(&s, length);
            _expirationDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
    }
}];
_inAppPurchases = purchases;

Getting the in-app purchases

Each in-app purchase is also in ASN1. Parsing it is very similar than parsing the general receipt information.

From RMAppReceipt, using the same helper methods:

[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) {
    const uint8_t *p = data.bytes;
    const NSUInteger length = data.length;
    switch (type)
    {
        case RMAppReceiptASN1TypeQuantity:
            _quantity = RMASN1ReadInteger(&p, length);
            break;
        case RMAppReceiptASN1TypeProductIdentifier:
            _productIdentifier = RMASN1ReadUTF8String(&p, length);
            break;
        case RMAppReceiptASN1TypeTransactionIdentifier:
            _transactionIdentifier = RMASN1ReadUTF8String(&p, length);
            break;
        case RMAppReceiptASN1TypePurchaseDate:
        {
            NSString *string = RMASN1ReadIA5SString(&p, length);
            _purchaseDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
        case RMAppReceiptASN1TypeOriginalTransactionIdentifier:
            _originalTransactionIdentifier = RMASN1ReadUTF8String(&p, length);
            break;
        case RMAppReceiptASN1TypeOriginalPurchaseDate:
        {
            NSString *string = RMASN1ReadIA5SString(&p, length);
            _originalPurchaseDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
        case RMAppReceiptASN1TypeSubscriptionExpirationDate:
        {
            NSString *string = RMASN1ReadIA5SString(&p, length);
            _subscriptionExpirationDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
        case RMAppReceiptASN1TypeWebOrderLineItemID:
            _webOrderLineItemID = RMASN1ReadInteger(&p, length);
            break;
        case RMAppReceiptASN1TypeCancellationDate:
        {
            NSString *string = RMASN1ReadIA5SString(&p, length);
            _cancellationDate = [RMAppReceipt formatRFC3339String:string];
            break;
        }
    }
}];

It should be noted that certain in-app purchases, such as consumables and non-renewable subscriptions, will appear only once in the receipt. You should verify these right after the purchase (again, RMStore helps you with this).

Verification at a glance

Now we got all the fields from the receipt and all its in-app purchases. First we verify the receipt itself, and then we simply check if the receipt contains the product of the transaction.

Below is the method that we called back at the beginning. From RMStoreAppReceiptVerificator:

- (BOOL)verifyTransaction:(SKPaymentTransaction*)transaction
                inReceipt:(RMAppReceipt*)receipt
                           success:(void (^)())successBlock
                           failure:(void (^)(NSError *error))failureBlock
{
    const BOOL receiptVerified = [self verifyAppReceipt:receipt];
    if (!receiptVerified)
    {
        [self failWithBlock:failureBlock message:NSLocalizedString(@"The app receipt failed verification", @"")];
        return NO;
    }
    SKPayment *payment = transaction.payment;
    const BOOL transactionVerified = [receipt containsInAppPurchaseOfProductIdentifier:payment.productIdentifier];
    if (!transactionVerified)
    {
        [self failWithBlock:failureBlock message:NSLocalizedString(@"The app receipt doest not contain the given product", @"")];
        return NO;
    }
    if (successBlock)
    {
        successBlock();
    }
    return YES;
}

Verifying the receipt

Verifying the receipt itself boils down to:

  1. Checking that the receipt is valid PKCS7 and ASN1. We have done this implicitly already.
  2. Verifying that the receipt is signed by Apple. This was done before parsing the receipt and will be detailed below.
  3. Checking that the bundle identifier included in the receipt corresponds to your bundle identifier. You should hardcode your bundle identifier, as it doesn't seem to be very difficult to modify your app bundle and use some other receipt.
  4. Checking that the app version included in the receipt corresponds to your app version identifier. You should hardcode the app version, for the same reasons indicated above.
  5. Check the receipt hash to make sure the receipt correspond to the current device.

The 5 steps in code at a high-level, from RMStoreAppReceiptVerificator:

- (BOOL)verifyAppReceipt:(RMAppReceipt*)receipt
{
    // Steps 1 & 2 were done while parsing the receipt
    if (!receipt) return NO;

    // Step 3
    if (![receipt.bundleIdentifier isEqualToString:self.bundleIdentifier]) return NO;

    // Step 4
    if (![receipt.appVersion isEqualToString:self.bundleVersion]) return NO;

    // Step 5
    if (![receipt verifyReceiptHash]) return NO;

    return YES;
}

Let's drill-down into steps 2 and 5.

Verifying the receipt signature

Back when we extracted the data we glanced over the receipt signature verification. The receipt is signed with the Apple Inc. Root Certificate, which can be downloaded from Apple Root Certificate Authority. The following code takes the PKCS7 container and the root certificate as data and checks if they match:

+ (BOOL)verifyPKCS7:(PKCS7*)container withCertificateData:(NSData*)certificateData
{ // Based on: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW17
    static int verified = 1;
    int result = 0;
    OpenSSL_add_all_digests(); // Required for PKCS7_verify to work
    X509_STORE *store = X509_STORE_new();
    if (store)
    {
        const uint8_t *certificateBytes = (uint8_t *)(certificateData.bytes);
        X509 *certificate = d2i_X509(NULL, &certificateBytes, (long)certificateData.length);
        if (certificate)
        {
            X509_STORE_add_cert(store, certificate);

            BIO *payload = BIO_new(BIO_s_mem());
            result = PKCS7_verify(container, NULL, store, NULL, payload, 0);
            BIO_free(payload);

            X509_free(certificate);
        }
    }
    X509_STORE_free(store);
    EVP_cleanup(); // Balances OpenSSL_add_all_digests (), per http://www.openssl.org/docs/crypto/OpenSSL_add_all_algorithms.html

    return result == verified;
}

This was done back at the beginning, before the receipt was parsed.

Verifying the receipt hash

The hash included in the receipt is a SHA1 of the device id, some opaque value included in the receipt and the bundle id.

This is how you would verify the receipt hash on iOS. From RMAppReceipt:

- (BOOL)verifyReceiptHash
{
    // TODO: Getting the uuid in Mac is different. See: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
    NSUUID *uuid = [[UIDevice currentDevice] identifierForVendor];
    unsigned char uuidBytes[16];
    [uuid getUUIDBytes:uuidBytes];

    // Order taken from: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
    NSMutableData *data = [NSMutableData data];
    [data appendBytes:uuidBytes length:sizeof(uuidBytes)];
    [data appendData:self.opaqueValue];
    [data appendData:self.bundleIdentifierData];

    NSMutableData *expectedHash = [NSMutableData dataWithLength:SHA_DIGEST_LENGTH];
    SHA1(data.bytes, data.length, expectedHash.mutableBytes);

    return [expectedHash isEqualToData:self.hash];
}

And that's the gist of it. I might be missing something here or there, so I might come back to this post later. In any case, I recommend browsing the complete code for more details.

这篇关于一个完整的解决方案,可以在iOS 7上验证应用内收据和捆绑收据的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

1403页,肝出来的..

09-06 16:29