확인 메일을 사용하여 제품 구매 검증

제품 구매를 성공적으로 이행한 각 Microsoft Store 거래에서 거래 영수증을 선택적으로 반환할 수 있습니다. 이 영수증은 고객에게 나열된 제품과 금전적 비용에 대한 정보를 제공합니다.

사용자가 앱을 구매했거나, Microsoft Store에서 추가 기능(앱에서 바로 구매 제품 또는 IAP라고도 함)을 구매했는지 확인해야 하는 경우 이 정보에 액세스할 수 있습니다. 예를 들어 다운로드한 콘텐츠를 제공하는 게임을 생각해 보겠습니다. 게임 콘텐츠를 구매한 사용자가 다른 디바이스에서 플레이하려는 경우 사용자가 이미 콘텐츠를 소유하고 있는지 확인해야 합니다. 방법은 다음과 같습니다.

Important

이 문서에서는 Windows.ApplicationModel.Store 네임스페이스의 멤버를 사용하여 앱에서 바로 구매의 영수증을 가져오고 유효성을 검사하는 방법을 보여 줍니다. 앱에서 바로 구매(Windows 10, 버전 1607에 도입되었으며 Visual Studio에서 Windows 10 Anniversary Edition(10.0, 빌드 14393) 이상 릴리스를 대상으로 하는 프로젝트에 사용 가능)를 위해 Windows.Services.Store 네임스페이스를 사용하는 경우, 이 네임스페이스는 앱에서 바로 구매 건의 영수증을 받기 위해 API를 제공하지 않습니다. 그러나 Microsoft Store 컬렉션 API의 REST 메서드를 사용하여 구매 거래 데이터를 가져올 수 있습니다. 자세한 내용은 앱 내 구매 영수증을 참조하세요.

영수증 요청

Windows.ApplicationModel.Store 네임스페이스는 영수증을 받는 여러 가지 방법을 지원합니다.

앱 영수증은 다음과 같이 표시됩니다.

참고 항목

이 예제는 XML을 쉽게 읽을 수 있도록 형식화되었습니다. 실제 앱 영수증에는 요소 사이에 공백이 포함되지 않습니다.

<Receipt Version="1.0" ReceiptDate="2012-08-30T23:10:05Z" CertificateId="b809e47cd0110a4db043b3f73e83acd917fe1336" ReceiptDeviceId="4e362949-acc3-fe3a-e71b-89893eb4f528">
    <AppReceipt Id="8ffa256d-eca8-712a-7cf8-cbf5522df24b" AppId="55428GreenlakeApps.CurrentAppSimulatorEventTest_z7q3q7z11crfr" PurchaseDate="2012-06-04T23:07:24Z" LicenseType="Full" />
    <ProductReceipt Id="6bbf4366-6fb2-8be8-7947-92fd5f683530" ProductId="Product1" PurchaseDate="2012-08-30T23:08:52Z" ExpirationDate="2012-09-02T23:08:49Z" ProductType="Durable" AppId="55428GreenlakeApps.CurrentAppSimulatorEventTest_z7q3q7z11crfr" />
    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
            <Reference URI="">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                <DigestValue>cdiU06eD8X/w1aGCHeaGCG9w/kWZ8I099rw4mmPpvdU=</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>SjRIxS/2r2P6ZdgaR9bwUSa6ZItYYFpKLJZrnAa3zkMylbiWjh9oZGGng2p6/gtBHC2dSTZlLbqnysJjl7mQp/A3wKaIkzjyRXv3kxoVaSV0pkqiPt04cIfFTP0JZkE5QD/vYxiWjeyGp1dThEM2RV811sRWvmEs/hHhVxb32e8xCLtpALYx3a9lW51zRJJN0eNdPAvNoiCJlnogAoTToUQLHs72I1dECnSbeNPXiG7klpy5boKKMCZfnVXXkneWvVFtAA1h2sB7ll40LEHO4oYN6VzD+uKd76QOgGmsu9iGVyRvvmMtahvtL1/pxoxsTRedhKq6zrzCfT8qfh3C1w==</SignatureValue>
    </Signature>
</Receipt>

제품 영수증은 다음과 같이 표시됩니다.

참고 항목

이 예제는 XML을 쉽게 읽을 수 있도록 형식화되었습니다. 실제 제품 영수증에는 요소 사이에 공백이 포함되지 않습니다.

<Receipt Version="1.0" ReceiptDate="2012-08-30T23:08:52Z" CertificateId="b809e47cd0110a4db043b3f73e83acd917fe1336" ReceiptDeviceId="4e362949-acc3-fe3a-e71b-89893eb4f528">
    <ProductReceipt Id="6bbf4366-6fb2-8be8-7947-92fd5f683530" ProductId="Product1" PurchaseDate="2012-08-30T23:08:52Z" ExpirationDate="2012-09-02T23:08:49Z" ProductType="Durable" AppId="55428GreenlakeApps.CurrentAppSimulatorEventTest_z7q3q7z11crfr" />
    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
            <Reference URI="">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                <DigestValue>Uvi8jkTYd3HtpMmAMpOm94fLeqmcQ2KCrV1XmSuY1xI=</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>TT5fDET1X9nBk9/yKEJAjVASKjall3gw8u9N5Uizx4/Le9RtJtv+E9XSMjrOXK/TDicidIPLBjTbcZylYZdGPkMvAIc3/1mdLMZYJc+EXG9IsE9L74LmJ0OqGH5WjGK/UexAXxVBWDtBbDI2JLOaBevYsyy+4hLOcTXDSUA4tXwPa2Bi+BRoUTdYE2mFW7ytOJNEs3jTiHrCK6JRvTyU9lGkNDMNx9loIr+mRks+BSf70KxPtE9XCpCvXyWa/Q1JaIyZI7llCH45Dn4SKFn6L/JBw8G8xSTrZ3sBYBKOnUDbSCfc8ucQX97EyivSPURvTyImmjpsXDm2LBaEgAMADg==</SignatureValue>
    </Signature>
</Receipt>

이러한 영수증 예 중 하나를 사용하여 유효성 검사 코드를 테스트할 수 있습니다. 영수증의 내용에 대한 자세한 내용은 요소 및 특성 설명을 참조하세요.

영수증 유효성 검사

영수증 신뢰성의 유효성을 검사하려면 공용 인증서를 사용하여 영수증의 서명을 확인할 수 있는 백 엔드 시스템(웹 서비스 또는 이와 유사한 시스템)이 필요합니다. 이 인증서를 가져오려면 영수증의 CertificateId이(가) CertificateId 값인 URL https://lic.apps.microsoft.com/licensing/certificateserver/?cid=CertificateId%60%60%60을 사용합니다.

다음은 해당 유효성 검사 프로세스의 예입니다. 이 코드는 System.Security 어셈블리에 대한 참조를 포함하는 .NET Framework 콘솔 애플리케이션에서 실행됩니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
using System.IO;
using System.Security.Cryptography.Xml;
using System.Net;

namespace ReceiptVerificationSample
{
    public sealed class RSAPKCS1SHA256SignatureDescription : SignatureDescription
    {
        public RSAPKCS1SHA256SignatureDescription()
        {
            base.KeyAlgorithm = typeof(RSACryptoServiceProvider).FullName;
            base.DigestAlgorithm = typeof(SHA256Managed).FullName;
            base.FormatterAlgorithm = typeof(RSAPKCS1SignatureFormatter).FullName;
            base.DeformatterAlgorithm = typeof(RSAPKCS1SignatureDeformatter).FullName;
        }

        public override AsymmetricSignatureDeformatter CreateDeformatter(AsymmetricAlgorithm key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            RSAPKCS1SignatureDeformatter deformatter = new RSAPKCS1SignatureDeformatter(key);
            deformatter.SetHashAlgorithm("SHA256");
            return deformatter;
        }

        public override AsymmetricSignatureFormatter CreateFormatter(AsymmetricAlgorithm key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            RSAPKCS1SignatureFormatter formatter = new RSAPKCS1SignatureFormatter(key);
            formatter.SetHashAlgorithm("SHA256");
            return formatter;
        }
    }

    class Program
    {
        // Utility function to read the bytes from an HTTP response
        private static int ReadResponseBytes(byte[] responseBuffer, Stream resStream)
        {
            int count = 0;
            int numBytesRead = 0;
            int numBytesToRead = responseBuffer.Length;

            do
            {
                count = resStream.Read(responseBuffer, numBytesRead, numBytesToRead);
                numBytesRead += count;
                numBytesToRead -= count;
            } while (count > 0);

            return numBytesRead;
        }

        public static X509Certificate2 RetrieveCertificate(string certificateId)
        {
            const int MaxCertificateSize = 10000;

            // Retrieve the certificate URL.
            String certificateUrl = String.Format(
                "https://go.microsoft.com/fwlink/?LinkId=246509&cid={0}", certificateId);

            // Make an HTTP GET request for the certificate
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(certificateUrl);
            request.Method = "GET";

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();

            // Retrieve the certificate out of the response stream
            byte[] responseBuffer = new byte[MaxCertificateSize];
            Stream resStream = response.GetResponseStream();
            int bytesRead = ReadResponseBytes(responseBuffer, resStream);

            if (bytesRead < 1)
            {
                //TODO: Handle error here
            }

            return new X509Certificate2(responseBuffer);
        }

        static bool ValidateXml(XmlDocument receipt, X509Certificate2 certificate)
        {
            // Create the signed XML object.
            SignedXml sxml = new SignedXml(receipt);

            // Get the XML Signature node and load it into the signed XML object.
            XmlNode dsig = receipt.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl)[0];
            if (dsig == null)
            {
                // If signature is not found return false
                System.Console.WriteLine("Signature not found.");
                return false;
            }

            sxml.LoadXml((XmlElement)dsig);

            // Check the signature
            bool isValid = sxml.CheckSignature(certificate, true);

            return isValid;
        }

        static void Main(string[] args)
        {
            // .NET does not support SHA256-RSA2048 signature verification by default, 
            // so register this algorithm for verification.
            CryptoConfig.AddAlgorithm(typeof(RSAPKCS1SHA256SignatureDescription), 
                "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");

            // Load the receipt that needs to be verified as an XML document
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load("..\\..\\receipt.xml");

            // The certificateId attribute is present in the document root, retrieve it
            XmlNode node = xmlDoc.DocumentElement;
            string certificateId = node.Attributes["CertificateId"].Value;

            // Retrieve the certificate from the official site.
            // NOTE: For sake of performance, you would want to cache this certificate locally.
            //       Otherwise, every single call will incur the delay of certificate retrieval.
            X509Certificate2 verificationCertificate = RetrieveCertificate(certificateId);

            try
            {
                // Validate the receipt with the certificate retrieved earlier
                bool isValid = ValidateXml(xmlDoc, verificationCertificate);
                System.Console.WriteLine("Certificate valid: " + isValid);
            }
            catch (Exception ex)
            {
                System.Console.WriteLine(ex.ToString());
            }
        }
    }
}

영수증의 요소 및 특성 설명

이 섹션에서는 영수증의 요소와 특성에 대해 설명합니다.

영수증 요소

이 파일의 루트 요소는 앱 및 앱 내 구매에 대한 정보를 포함하는 영수증 요소입니다. 이 요소에는 다음의 자식 요소가 포함됩니다.

요소 필수 수량 설명
AppReceipt 아니요 0 또는 1 현재 앱에 대한 구매 정보를 포함합니다.
ProductReceipt 아니요 0 이상 현재 앱의 앱 내 구매에 대한 정보를 포함합니다.
서명 1 이 요소는 표준 XML-DSIG 구문입니다. 영수증의 유효성을 검사하는 데 사용할 수 있는 서명이 포함된 SignatureValue 요소와 SignedInfo 요소가 포함됩니다.

영수증에는 다음과 같은 특성이 있습니다.

attribute Description
버전 영수증의 버전 번호.
CertificateId 영수증에 서명하는 데 사용되는 인증서 지문입니다.
ReceiptDate 영수증이 서명되고 다운로드된 날짜입니다.
ReceiptDeviceId 이 영수증을 요청하는 데 사용되는 디바이스를 식별합니다.

AppReceipt 요소

이 요소에는 현재 앱에 대한 구매 정보가 포함됩니다.

AppReceipt에는 다음과 같은 특성이 있습니다.

attribute 설명
ID 구매를 식별합니다.
AppId OS에서 앱에 사용하는 패키지 패밀리 이름 값입니다.
LicenseType 사용자가 앱의 전체 버전을 구매한 경우 전체입니다. 사용자가 평가판 버전의 앱을 다운로드한 경우 평가판입니다.
PurchaseDate 앱을 획득한 날짜입니다.

ProductReceipt 요소

이 요소는 현재 앱의 앱 내 구매에 대한 정보를 포함합니다.

ProductReceipt에는 다음과 같은 특성이 있습니다.

attribute 설명
ID 구매를 식별합니다.
AppId 사용자가 어떤 앱을 통해 구매했는지 식별합니다.
ProductId 구매한 제품을 식별합니다.
ProductType 제품 유형을 결정합니다. 현재는 지속성 값만 지원합니다.
PurchaseDate 구매가 발생한 날짜입니다.