구매 영수증
스토어 확장(Store Extensions)

영수증 확인(Receipt validation)

영수증 확인을 통해 사용자가 구매하지 않은 콘텐츠에 액세스하지 못하게 합니다.

확인 시점

영수증 검증은 애플리케이션 콘텐츠가 배포되는 시점에 수행하는 것이 좋습니다.

  • Local Validation: 클라이언트측 콘텐츠인 경우 모든 콘텐츠가 애플리케이션에 포함되고, 구매 이후 모두 활성화되며, 원격 서버에 연결할 필요 없이 타겟 장치에서 확인을 진행해야 합니다. Unity IAP는 애플리케이션에서 로컬 확인을 지원합니다. 자세한 내용은 아래의 Local Validation 부분을 참조하십시오.
  • Remote Validation: 서버측 콘텐츠인 경우, 구매 이후 콘텐츠가 다운로드되고, 콘텐츠가 릴리스되기 이전 서버에서 확인을 진행해야 합니다. Unity는 서버측 확인을 지원하지 않지만, Nobuyori Takahashi’s IAP project와 같은 서드 파티 솔루션은 지원합니다.

로컬 확인

사용자가 구매한 콘텐츠가 장치에 이미 존재하는 경우, 애플리케이션은 콘텐츠의 해지 여부만 결정하면 됩니다.

Unity IAP는 Google Play와 Apple 스토어를 통해 콘텐츠를 숨기고 영수증을 파싱할 수 있는 툴을 제공합니다.

암호화 키 난독 처리

영수증 검증은 알려진 암호화 키를 사용해 진행됩니다. 이 키는 애플리케이션의 암호화된 Google Play 공용 키나 Apple 루트 인증서입니다.

사용자가 이 키를 교체할 수 있다면 영수증 확인 과정을 무력화할 수 있으므로, 사용자가 키를 찾아 수정하기 어렵도록 해야 합니다.

Unity IAP는 애플리케이션에서 암호화 키를 난독 처리할 수 있는 툴을 제공합니다. 이 툴은 키를 난독 처리하여 사용자가 액세스하기 어렵게 만듭니다. Unity 메뉴 바에서 Window > Unity IAP > IAP Receipt Validation Obfuscator 로 가야 합니다.

난독 처리 창
난독 처리 창

이 창은 Unity IAP와 번들된 Apple 루트 인증서와 애플리케이션의 Google Play Developer Console’s Services & APIs 페이지에서의 Google Play 공용 키를 둘 다 암호화하여 AppleTangleGooglePlayTangle 이라는 두 개의 다른 C# 파일에 저장합니다. 이 파일은 프로젝트에 추가되어 다음 섹션에서 사용하게 됩니다.

Apple 스토어만 타겟하는 경우에는 Google Play 공용 키를 제공할 필요가 없으며, 그 반대도 마찬가지입니다.

영수증 검증

CrossPlatformValidator 클래스를 사용하여 Google Play와 Apple 스토어에서 확인을 수행할 수 있습니다.

클래스에 Google Play 공용 키나 Apple 루트 인증서를 제공해야 하며, 두 플랫폼에서 확인을 수행하려면 둘 다 제공해야 합니다.

CrossPlatformValidator는 다음 두 개의 검사를 수행합니다.

  • 서명 검증을 통해 영수증 유효성을 검사합니다.
  • 영수증의 애플리케이션 번들 식별자를 애플리케이션의 식별자와 비교합니다. 이 둘이 일치하지 않으면 InvalidBundleId 예외 오류가 발생합니다.

이 검증기는 Google Play와 Apple 플랫폼에서 생성된 영수증만 확인할 수 있습니다. 다른 플랫폼에서 생성한 영수증이나 에디터에서 생성한 모의 영수증을 확인하면 IAPSecurityException 오류가 발생합니다.

비밀 키를 제공하지 않은 플랫폼의 영수증을 확인하려고 시도하면 MissingStoreSecretException 오류가 발생합니다.

public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
    bool validPurchase = true; // Presume valid for platforms with no R.V.

    // Unity IAP's validation logic is only included on these platforms.
#if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX
    // Prepare the validator with the secrets we prepared in the Editor
    // obfuscation window.
    var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
        AppleTangle.Data(), Application.bundleIdentifier);

    try {
        // On Google Play, result has a single product ID.
        // On Apple stores, receipts contain multiple products.
        var result = validator.Validate(e.purchasedProduct.receipt);
        // For informational purposes, we list the receipt(s)
        Debug.Log("Receipt is valid. Contents:");
        foreach (IPurchaseReceipt productReceipt in result) {
            Debug.Log(productReceipt.productID);
            Debug.Log(productReceipt.purchaseDate);
            Debug.Log(productReceipt.transactionID);
        }
    } catch (IAPSecurityException) {
        Debug.Log("Invalid receipt, not unlocking content");
        validPurchase = false;
    }
#endif

    if (validPurchase) {
        // Unlock the appropriate content here.
    }

    return PurchaseProcessingResult.Complete;
}

영수증의 유효성뿐만 아니라 해당 내용도 검사해야 합니다. 사용자가 구매를 하지 않고 콘텐츠에 액세스하는 흔한 방법 중 하나는 다른 제품이나 애플리케이션에서 영수증을 제공하는 것입니다. 영수증은 진품이고 확인 과정을 통과하기 때문에, CrossPlatformValidator 가 제품 ID를 파싱한 결과에 따라 판단을 내리도록 해야 합니다.

스토어별 세부 사항

스토어마다 구매 영수증을 구성하는 필드가 다릅니다. 스토어별 필드에 액세스하려면 IPurchaseReceipt를 두 개의 서브타입인 GooglePlayReceiptAppleInAppPurchaseReceipt에 적용하면 됩니다.

var result = validator.Validate(e.purchasedProduct.receipt);
Debug.Log("Receipt is valid. Contents:");
foreach (IPurchaseReceipt productReceipt in result) {
    Debug.Log(productReceipt.productID);
    Debug.Log(productReceipt.purchaseDate);
    Debug.Log(productReceipt.transactionID);

    GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
    if (null != google) {
        // This is Google's Order ID.
        // Note that it is null when testing in the sandbox
        // because Google's sandbox does not provide Order IDs.
        Debug.Log(google.transactionID);
        Debug.Log(google.purchaseState);
        Debug.Log(google.purchaseToken);
    }

    AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
    if (null != apple) {
        Debug.Log(apple.originalTransactionIdentifier);
        Debug.Log(apple.subscriptionExpirationDate);
        Debug.Log(apple.cancellationDate);
        Debug.Log(apple.quantity);
    }
}

raw 상태의 Apple 영수증 파싱

AppleValidator 클래스를 사용하여 Apple 영수증 세부 정보를 추출할 수 있습니다. 이 클래스는 버전 7.0 이후의 iOS 앱 영수증에만 작동하며 그 이전 버전의 영수증에서는 작동하지 않습니다.

#if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
// Get a reference to IAppleConfiguration during IAP initialization.
var appleConfig = builder.Configure<IAppleConfiguration>();
var receiptData = System.Convert.FromBase64String(appleConfig.appReceipt);
AppleReceipt receipt = new AppleValidator(AppleTangle.Data()).Validate(receiptData);

Debug.Log(receipt.bundleID);
Debug.Log(receipt.receiptCreationDate);
foreach (AppleInAppPurchaseReceipt productReceipt in receipt.inAppPurchaseReceipts) {
    Debug.Log(productReceipt.transactionIdentifier);
    Debug.Log(productReceipt.productIdentifier);
}
#endif

AppleReceipt 타입은 Apple의 ASN1 영수증 포맷을 모델링합니다. 각각의 필드에 대한 자세한 내용은 Apple 문서를 참조하십시오.

구매 영수증
스토어 확장(Store Extensions)