โซลูชันที่สมบูรณ์สำหรับ LOCALLY ตรวจสอบใบเสร็จรับเงินในแอพและใบเสร็จรับเงินแบบมัดบน iOS 7


160

ฉันได้อ่านเอกสารและรหัสจำนวนมากซึ่งในทางทฤษฎีแล้วจะตรวจสอบใบเสร็จในแอปและ / หรือใบเสร็จจากชุดรวม

เนื่องจากความรู้ของฉันเกี่ยวกับ SSL ใบรับรองการเข้ารหัส ฯลฯ เกือบเป็นศูนย์คำอธิบายทั้งหมดที่ฉันได้อ่านเช่นเดียวกับที่มีแนวโน้มนี้ฉันพบว่ายากที่จะเข้าใจ

พวกเขาบอกว่าคำอธิบายนั้นไม่สมบูรณ์เพราะทุกคนต้องหาวิธีที่จะทำหรือแฮกเกอร์จะมีงานง่าย ๆ ในการสร้างแอพแครกเกอร์ที่สามารถจดจำและระบุรูปแบบและแก้ไขแอปพลิเคชันได้ ตกลงฉันเห็นด้วยกับสิ่งนี้จนถึงจุดหนึ่ง ฉันคิดว่าพวกเขาสามารถอธิบายได้อย่างสมบูรณ์ถึงวิธีการทำและแจ้งเตือนว่า "แก้ไขวิธีนี้", "แก้ไขวิธีอื่น", "ทำให้ตัวแปรนี้สับสน", "เปลี่ยนชื่อของสิ่งนี้และ" เป็นต้น

มีวิญญาณที่ดีออกมาได้ไหมที่จะอธิบายวิธีการตรวจสอบในท้องถิ่น, ใบเสร็จรับเงินมัดและใบเสร็จรับเงินการซื้อภายในแอปใน iOS 7ในขณะที่ฉันอายุห้าขวบ (ตกลงทำ 3) จากบนลงล่างชัดเจน?

ขอบคุณ !!!


หากคุณมีเวอร์ชันที่ทำงานกับแอพของคุณและข้อกังวลของคุณก็คือแฮกเกอร์จะเห็นวิธีการใช้งานของคุณเพียงแค่เปลี่ยนวิธีการที่ละเอียดอ่อนก่อนที่จะเผยแพร่ที่นี่ ทำให้งงสตริงเปลี่ยนลำดับของบรรทัดเปลี่ยนวิธีการวนรอบ (จากการใช้เพื่อบล็อกการแจงนับและกลับกัน) และสิ่งต่าง ๆ เช่นนั้น เห็นได้ชัดว่าทุกคนที่ใช้รหัสที่อาจโพสต์ที่นี่ต้องทำสิ่งเดียวกันไม่เสี่ยงที่จะถูกแฮ็คได้ง่าย


1
คำเตือนอย่างเป็นธรรม: การทำในพื้นที่ทำให้การแก้ไขฟังก์ชั่นนี้ง่ายกว่าแอพพลิเคชั่นของคุณ
NinjaLikesCheez

2
ตกลงฉันรู้ แต่ประเด็นที่นี่คือการทำสิ่งที่ยากและป้องกันการแคร็ก / ปะอัตโนมัติ คำถามคือหากแฮ็กเกอร์ต้องการที่จะถอดรหัสแอปของคุณเขา / เธอจะทำมันไม่ว่าวิธีการใดที่คุณใช้ในท้องถิ่นหรือระยะไกล แนวคิดก็คือการเปลี่ยนแปลงเล็กน้อยทุกเวอร์ชั่นใหม่ที่คุณวางจำหน่ายเพื่อป้องกันการแก้ไขอัตโนมัติอีกครั้ง
Duck

4
@NinjaLikesCheez - หนึ่งสามารถ NOP ตรวจสอบแม้ว่าการตรวจสอบจะทำบนเซิร์ฟเวอร์
Duck

14
ขออภัย แต่นี่ไม่ใช่ข้อแก้ตัว สิ่งเดียวที่ผู้เขียนต้องทำคือการบอกว่าอย่าใช้รหัสตามที่เป็นอยู่ ไม่มีตัวอย่างใด ๆ ที่เป็นไปไม่ได้ที่จะเข้าใจสิ่งนี้โดยไม่ต้องเป็นนักวิทยาศาสตร์จรวด
Duck

3
หากคุณไม่ต้องการรบกวนการใช้ DRM อย่ากังวลกับการยืนยันในท้องถิ่น เพียงโพสต์ใบเสร็จรับเงินโดยตรงไปยัง Apple จากแอปของคุณและพวกเขาจะส่งกลับมาให้คุณอีกครั้งในรูปแบบ JSON ที่แยกวิเคราะห์ได้ง่าย มันเป็นเรื่องไม่สำคัญสำหรับโจรสลัดที่จะถอดรหัสสิ่งนี้ แต่ถ้าคุณเพิ่งจะเปลี่ยนเป็น freemium และไม่สนใจเรื่องการละเมิดลิขสิทธิ์มันเป็นเพียงโค้ดไม่กี่บรรทัดเท่านั้น
Dan Fabulich

คำตอบ:


146

นี่เป็นคำแนะนำว่าฉันจะแก้ไขปัญหานี้ได้อย่างไรในห้องสมุดการซื้อในแอปRMS ของฉัน ฉันจะอธิบายวิธีการตรวจสอบการทำธุรกรรมซึ่งรวมถึงการตรวจสอบใบเสร็จรับเงินทั้งหมด

อย่างรวดเร็ว

รับใบเสร็จและตรวจสอบการทำธุรกรรม ถ้ามันล้มเหลวรีเฟรชใบเสร็จรับเงินและลองอีกครั้ง สิ่งนี้ทำให้กระบวนการตรวจสอบแบบอะซิงโครนัสเป็นการรีเฟรชการรับแบบอะซิงโครนัส

จาก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];
}];

รับข้อมูลใบเสร็จรับเงิน

ใบเสร็จรับเงินอยู่ใน[[NSBundle mainBundle] appStoreReceiptURL]และเป็นจริงคอนเทนเนอร์ PCKS7 ฉันดูดที่เข้ารหัสดังนั้นฉันใช้ OpenSSL เพื่อเปิดคอนเทนเนอร์นี้ เห็นได้ชัดว่าคนอื่น ๆ ได้ทำมันได้อย่างหมดจดด้วยกรอบระบบ

การเพิ่ม OpenSSL ให้กับโครงการของคุณไม่สำคัญ RMStore วิกิพีเดียจะช่วยให้

หากคุณเลือกใช้ OpenSSL เพื่อเปิดคอนเทนเนอร์ PKCS7 รหัสของคุณอาจมีลักษณะเช่นนี้ จาก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;
}

เราจะเข้าไปดูรายละเอียดของการตรวจสอบในภายหลัง

รับข้อมูลใบเสร็จรับเงิน

การรับจะแสดงในรูปแบบ ASN1 มันมีข้อมูลทั่วไปบางสาขาเพื่อการตรวจสอบ (เราจะมาถึงในภายหลัง) และข้อมูลเฉพาะของการซื้อในแอพที่ใช้บังคับแต่ละรายการ

อีกครั้ง OpenSSL มาช่วยเหลือเมื่อมันมาอ่าน ASN1 จากRMAppReceiptใช้วิธีช่วยเหลือสองสามวิธี:

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;

รับการซื้อในแอป

การซื้อในแอปแต่ละครั้งจะอยู่ใน ASN1 ด้วย การแยกวิเคราะห์จะคล้ายกันมากกับการแยกวิเคราะห์ข้อมูลใบเสร็จทั่วไป

จากRMAppReceiptใช้วิธีการช่วยเหลือแบบเดียวกัน:

[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;
        }
    }
}]; 

ควรสังเกตว่าการซื้อในแอปบางอย่างเช่นการสิ้นเปลืองและการสมัครสมาชิกที่ไม่สามารถต่ออายุได้จะปรากฏเพียงครั้งเดียวในใบเสร็จรับเงิน คุณควรตรวจสอบสิทธิ์เหล่านี้หลังจากการซื้อ (อีกครั้ง RMStore ช่วยคุณทำสิ่งนี้)

ตรวจสอบได้อย่างรวดเร็ว

ตอนนี้เราได้รับทุกฟิลด์จากใบเสร็จและการซื้อในแอป ก่อนอื่นเราตรวจสอบใบเสร็จรับเงินจากนั้นเราก็ตรวจสอบว่าใบเสร็จรับเงินมีผลิตภัณฑ์ของธุรกรรมหรือไม่

ด้านล่างเป็นวิธีการที่เราเรียกกลับไปที่จุดเริ่มต้น จาก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;
}

การตรวจสอบใบเสร็จรับเงิน

การตรวจสอบใบเสร็จรับเงินทำให้เดือดลงไปที่:

  1. การตรวจสอบว่าการรับนั้นถูกต้อง PKCS7 และ ASN1 เราได้ทำสิ่งนี้โดยปริยายแล้ว
  2. การตรวจสอบว่าใบเสร็จรับเงินนั้นลงนามโดย Apple สิ่งนี้ทำก่อนการแยกวิเคราะห์ใบเสร็จและจะมีรายละเอียดด้านล่าง
  3. การตรวจสอบว่าตัวระบุบันเดิลที่รวมอยู่ในใบเสร็จนั้นตรงกับตัวระบุบันเดิลของคุณ คุณควร hardcode ตัวบ่งชี้กลุ่มของคุณเนื่องจากมันไม่ได้เป็นเรื่องยากมากที่จะแก้ไขกลุ่มแอพของคุณและใช้ใบเสร็จรับเงินอื่น ๆ
  4. การตรวจสอบว่าเวอร์ชันของแอพที่รวมอยู่ในใบเสร็จรับเงินนั้นตรงกับตัวระบุเวอร์ชันแอปของคุณ คุณควรเข้ารหัสเวอร์ชันแอปด้วยเหตุผลเดียวกันกับที่ระบุไว้ด้านบน
  5. ตรวจสอบแฮชใบเสร็จรับเงินเพื่อให้แน่ใจว่าใบเสร็จนั้นสอดคล้องกับอุปกรณ์ปัจจุบัน

โค้ด 5 ขั้นตอนในระดับสูงจาก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;
}

มาเจาะลึกในขั้นตอนที่ 2 และ 5

การตรวจสอบลายเซ็นใบเสร็จรับเงิน

ย้อนกลับไปเมื่อเราดึงข้อมูลที่เรามองผ่านการตรวจสอบลายเซ็นใบเสร็จ ใบเสร็จรับเงินมีการเซ็นสัญญากับแอปเปิ้ลอิงค์ใบรับรองหลักซึ่งสามารถดาวน์โหลดได้จากแอปเปิ้ล Root Certificate Authority รหัสต่อไปนี้ใช้คอนเทนเนอร์ PKCS7 และใบรับรองรูทเป็นข้อมูลและตรวจสอบว่าตรงกันหรือไม่:

+ (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;
}

สิ่งนี้ทำเสร็จตั้งแต่ต้นก่อนที่จะมีการแยกวิเคราะห์ใบเสร็จ

การตรวจสอบแฮชใบเสร็จรับเงิน

แฮชที่รวมอยู่ในใบเสร็จคือ SHA1 ของรหัสอุปกรณ์ค่าทึบแสงบางอย่างรวมอยู่ในใบเสร็จและรหัสบันเดิล

นี่คือวิธีที่คุณจะตรวจสอบความถูกต้องของใบเสร็จรับเงินบน iOS จาก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];
}

และนั่นคือส่วนสำคัญของมัน ฉันอาจจะหายไปบางสิ่งบางอย่างที่นี่หรือที่นั่นดังนั้นฉันอาจกลับมาที่โพสต์นี้ในภายหลัง ในกรณีใด ๆ ฉันขอแนะนำให้ดูรหัสที่สมบูรณ์สำหรับรายละเอียดเพิ่มเติม


2
การปฏิเสธความปลอดภัย: การใช้รหัสโอเพนซอร์สทำให้แอปของคุณมีความเสี่ยงมากขึ้น หากความปลอดภัยเป็นเรื่องที่กังวลคุณอาจต้องการใช้ RMStore และรหัสด้านบนเป็นแนวทางเท่านั้น
hpique

6
มันจะยอดเยี่ยมหากในอนาคตคุณจะกำจัด OpenSSL และทำให้ไลบรารีของคุณมีขนาดกะทัดรัดโดยใช้เพียงแค่เฟรมเวิร์กระบบ
Duck

2
@RubberDuck ดูgithub.com/robotmedia/RMStore/issues/16 รู้สึกอิสระที่จะพูดสอดหรือมีส่วนร่วม :)
hpique

1
@RubberDuck ฉันไม่มีความรู้เกี่ยวกับ OpenSSL จนกระทั่งสิ่งนี้ ใครจะรู้คุณอาจจะชอบมัน : P
hpique

2
มันไวต่อมนุษย์ในการโจมตีกลางซึ่งการร้องขอและ / หรือการตอบสนองสามารถถูกดักจับและแก้ไข ตัวอย่างเช่นคำขอสามารถเปลี่ยนเส้นทางไปยังเซิร์ฟเวอร์ของบุคคลที่สามและสามารถส่งคืนการตอบสนองที่ผิดพลาดโดยหลอกให้แอปคิดว่ามีการซื้อผลิตภัณฑ์เมื่อไม่ได้และเปิดใช้งานฟังก์ชั่นฟรี
Jasarien

13

ฉันประหลาดใจที่ไม่มีใครพูดถึงReceigenที่นี่ มันเป็นเครื่องมือที่สร้างรหัสการตรวจสอบใบเสร็จที่ยุ่งเหยิงโดยอัตโนมัติซึ่งแตกต่างกันในแต่ละครั้ง รองรับทั้ง GUI และการใช้งานบรรทัดคำสั่ง แนะนำเป็นอย่างยิ่ง

(ไม่เกี่ยวข้องกับ Receigen เพียงแค่ผู้ใช้ที่มีความสุข)

ฉันใช้ Rakefile เช่นนี้เพื่อเรียกใช้ Receigen อีกครั้งโดยอัตโนมัติ (เพราะต้องทำทุกครั้งที่มีการเปลี่ยนแปลง) เมื่อฉันพิมพ์rake receigen:

desc "Regenerate App Store Receipt validation code using Receigen app (which must be already installed)"
task :receigen do
  # TODO: modify these to match your app
  bundle_id = 'com.example.YourBundleIdentifierHere'
  output_file = File.join(__dir__, 'SomeDir/ReceiptValidation.h')

  version = PList.get(File.join(__dir__, 'YourProjectFolder/Info.plist'), 'CFBundleVersion')
  command = %Q</Applications/Receigen.app/Contents/MacOS/Receigen --identifier #{bundle_id} --version #{version} --os ios --prefix ReceiptValidation --success callblock --failure callblock>
  puts "#{command} > #{output_file}"
  data = `#{command}`
  File.open(output_file, 'w') { |f| f.write(data) }
end

module PList
  def self.get file_name, key
    if File.read(file_name) =~ %r!<key>#{Regexp.escape(key)}</key>\s*<string>(.*?)</string>!
      $1.strip
    else
      nil
    end
  end
end

1
สำหรับผู้ที่สนใจใน Receigen นี่เป็นวิธีการชำระเงินซึ่งมีอยู่ใน App Store ในราคา 29.99 $ แม้ว่าจะยังไม่ได้อัปเดตตั้งแต่เดือนกันยายน 2014
DevGansta

จริงการขาดการปรับปรุงน่ากลัวมาก อย่างไรก็ตามมันยังคงใช้งานได้; FWIW ฉันใช้มันในแอพของฉัน
Andrey Tarantsov

ตรวจสอบแอปของคุณเป็นเครื่องมือหารอยรั่วด้วย Receigen ฉันได้รับมาก
สาธุคุณ

Receigen เป็นคมตัด แต่ใช่มันเป็นความอัปยศที่ดูเหมือนว่าจะลดลง
Fattie

1
ดูเหมือนว่ามันจะไม่ลดลงเลย อัปเดตเมื่อสามสัปดาห์ที่ผ่านมา!
Oleg Korzhukov

2

หมายเหตุ: ไม่แนะนำให้ทำการตรวจสอบประเภทนี้ในฝั่งไคลเอ็นต์

นี่เป็นรุ่นSwift 4สำหรับการตรวจสอบใบเสร็จในการซื้อภายในแอป ...

ให้สร้าง enum เพื่อแสดงถึงข้อผิดพลาดที่เป็นไปได้ของการตรวจสอบการรับสินค้า

enum ReceiptValidationError: Error {
    case receiptNotFound
    case jsonResponseIsNotValid(description: String)
    case notBought
    case expired
}

จากนั้นเราจะสร้างฟังก์ชั่นที่ตรวจสอบความถูกต้องใบเสร็จรับเงินมันจะส่งข้อผิดพลาดถ้ามันไม่สามารถตรวจสอบได้

func validateReceipt() throws {
    guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) else {
        throw ReceiptValidationError.receiptNotFound
    }

    let receiptData = try! Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
    let receiptString = receiptData.base64EncodedString()
    let jsonObjectBody = ["receipt-data" : receiptString, "password" : <#String#>]

    #if DEBUG
    let url = URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")!
    #else
    let url = URL(string: "https://buy.itunes.apple.com/verifyReceipt")!
    #endif

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.httpBody = try! JSONSerialization.data(withJSONObject: jsonObjectBody, options: .prettyPrinted)

    let semaphore = DispatchSemaphore(value: 0)

    var validationError : ReceiptValidationError?

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data, let httpResponse = response as? HTTPURLResponse, error == nil, httpResponse.statusCode == 200 else {
            validationError = ReceiptValidationError.jsonResponseIsNotValid(description: error?.localizedDescription ?? "")
            semaphore.signal()
            return
        }
        guard let jsonResponse = (try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)) as? [AnyHashable: Any] else {
            validationError = ReceiptValidationError.jsonResponseIsNotValid(description: "Unable to parse json")
            semaphore.signal()
            return
        }
        guard let expirationDate = self.expirationDate(jsonResponse: jsonResponse, forProductId: <#String#>) else {
            validationError = ReceiptValidationError.notBought
            semaphore.signal()
            return
        }

        let currentDate = Date()
        if currentDate > expirationDate {
            validationError = ReceiptValidationError.expired
        }

        semaphore.signal()
    }
    task.resume()

    semaphore.wait()

    if let validationError = validationError {
        throw validationError
    }
}

ลองใช้ฟังก์ชั่นตัวช่วยนี้เพื่อรับวันหมดอายุของผลิตภัณฑ์เฉพาะ ฟังก์ชันรับการตอบกลับ JSON และรหัสผลิตภัณฑ์ การตอบกลับ JSON สามารถมีข้อมูลการรับหลายรายการสำหรับผลิตภัณฑ์ที่แตกต่างกันดังนั้นจึงได้รับข้อมูลล่าสุดสำหรับพารามิเตอร์ที่ระบุ

func expirationDate(jsonResponse: [AnyHashable: Any], forProductId productId :String) -> Date? {
    guard let receiptInfo = (jsonResponse["latest_receipt_info"] as? [[AnyHashable: Any]]) else {
        return nil
    }

    let filteredReceipts = receiptInfo.filter{ return ($0["product_id"] as? String) == productId }

    guard let lastReceipt = filteredReceipts.last else {
        return nil
    }

    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"

    if let expiresString = lastReceipt["expires_date"] as? String {
        return formatter.date(from: expiresString)
    }

    return nil
}

ตอนนี้คุณสามารถเรียกใช้ฟังก์ชันนี้และจัดการกับกรณีข้อผิดพลาดที่เป็นไปได้

do {
    try validateReceipt()
    // The receipt is valid 😌
    print("Receipt is valid")
} catch ReceiptValidationError.receiptNotFound {
    // There is no receipt on the device 😱
} catch ReceiptValidationError.jsonResponseIsNotValid(let description) {
    // unable to parse the json 🤯
    print(description)
} catch ReceiptValidationError.notBought {
    // the subscription hasn't being purchased 😒
} catch ReceiptValidationError.expired {
    // the subscription is expired 😵
} catch {
    print("Unexpected error: \(error).")
}

คุณสามารถรับรหัสผ่านได้จาก App Store Connect https://developer.apple.comเปิดลิงค์นี้คลิกที่

  • Account tab
  • Do Sign in
  • Open iTune Connect
  • Open My App
  • Open Feature Tab
  • Open In App Purchase
  • Click at the right side on 'View Shared Secret'
  • At the bottom you will get a secrete key

คัดลอกคีย์นั้นและวางในฟิลด์รหัสผ่าน

หวังว่านี้จะช่วยให้ทุกคนที่ต้องการในรุ่นที่รวดเร็ว


19
คุณไม่ควรใช้ URL การตรวจสอบของ Apple จากอุปกรณ์ของคุณ ควรใช้จากเซิร์ฟเวอร์ของคุณเท่านั้น เรื่องนี้ถูกกล่าวถึงในการประชุม WWDC
pechar

จะเกิดอะไรขึ้นหากผู้ใช้ลบแอปหรือไม่เปิดเป็นเวลานาน การคำนวณวันหมดอายุของคุณทำงานได้ดีหรือไม่?
karthikeyan

จากนั้นคุณต้องทำการตรวจสอบความถูกต้องทางฝั่งเซิร์ฟเวอร์
Pushpendra

1
ดังที่ @pechar กล่าวไว้คุณไม่ควรทำสิ่งนี้ โปรดเพิ่มเข้าไปในคำตอบของคุณ ดูเซสชัน WWDC ที่ 36:32 => developer.apple.com/videos/play/wwdc2016/702
cicerocamargo

ฉันไม่เข้าใจว่าทำไมการส่งข้อมูลใบเสร็จรับเงินโดยตรงจากอุปกรณ์จึงไม่ปลอดภัย ทุกคนจะสามารถอธิบายได้หรือไม่
เกาะ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.