Проверка покупок продуктов с помощью квитанций

Каждая транзакция Microsoft Store, которая заканчивается успешной сделкой, может дополнительно возвращать квитанцию транзакции. Эта квитанция предоставляет клиенту сведения об указанном продукте и денежных расходах.

Доступ к этой информации поддерживает сценарии, в которых ваше приложение должно проверить покупку пользователем приложения или надстройки (продукта из приложения или IAP) в Microsoft Store. Например, представим игру, предлагающую загруженное содержимое. Если пользователь, который приобрел игровое содержимое, захочет поиграть в эту игру на другом устройстве, то вам нужно будет проверить, действительно ли этот пользователь купил данное содержимое. Ниже описывается порядок действий.

Важно!

В этой статье показано, как использовать элементы пространства имен Windows.ApplicationModel.Store для получения и проверки квитанции о покупке из приложения. Если вы используете пространство имен Windows.Services.Store для покупок из приложения (появились в Windows 10 версии 1607 и доступны в проектах, предназначенных для Windows 10 Anniversary Edition (10.0; сборка 14393) или более поздней версии в Visual Studio), это пространство имен не предоставляет API-интерфейс для получения квитанций о покупках из приложения. Тем не менее для получения данных о транзакции покупки можно использовать метод REST в API коллекции Microsoft Store. Дополнительные сведения см. в разделе Квитанции для покупок из приложения.

Запрос квитанции

Пространство имен Windows.ApplicationModel.Store поддерживает несколько способов получения квитанции:

  • Когда вы совершаете покупку, используя метод CurrentApp.RequestAppPurchaseAsync или CurrentApp.RequestProductPurchaseAsync (или одну из других перегрузок этого метода), возвращаемое значение содержит квитанцию.
  • Можно вызвать метод CurrentApp.GetAppReceiptAsync, чтобы получить актуальные сведения о квитанциях для приложения и любых надстроек в нем.

Квитанция приложения выглядит так.

Примечание

Этот пример отформатирован, чтобы 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>

Для подтверждения кода проверки можно использовать любой из этих примеров квитанций. Дополнительные сведения о содержимом квитанции см. в разделе Описания элементов и атрибутов.

Проверка квитанции

Для проверки подлинности квитанции серверная система (веб-служба или аналогичный объект) должна проверить подпись на квитанции с помощью открытого сертификата. Чтобы получить этот сертификат, используйте URL-адрес https://lic.apps.microsoft.com/licensing/certificateserver/?cid=CertificateId%60%60%60, где CertificateId — значение CertificateId в квитанции.

Пример процедуры валидации приведен ниже. Этот код выполняется в консольном приложении .NET Framework, которое содержит ссылку на сборку System.Security.

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

Описания элементов и атрибутов для квитанции

В этом разделе описаны элементы и атрибуты в квитанции.

Элемент квитанции

Корневой элемент этого файла — это элемент Receipt, который содержит сведения о приложении и покупках из приложения. Этот элемент содержит следующие дочерние элементы.

Элемент Обязательно Количество Описание
AppReceipt Нет 0 или 1 Содержит информацию о покупках для текущего приложения.
ProductReceipt Нет 0 или больше Содержит сведения о покупке из приложения для текущего приложения.
Сигнатура Да 1 Этот элемент — стандартная конструкция XML-DSIG. Он содержит элемент SignatureValue с подписью, который можно использовать для проверки квитанции, а также элемент SignedInfo.

Receipt содержит следующие атрибуты.

attribute Описание
Версия Номер версии квитанции.
CertificateId Отпечаток сертификата, который используется для добавления подписи на квитанцию.
ReceiptDate Дата подписания и загрузки квитанции.
ReceiptDeviceId Определяет устройство, используемое для запроса этой квитанции.

Элемент AppReceipt

Этот элемент содержит информацию о покупках для текущего приложения.

AppReceipt содержит следующие атрибуты.

attribute Описание
Id Идентифицирует покупку.
Appid Значение имени семейства пакетов, используемое ОС для приложения.
LicenseType Полная, если пользователь приобрел полную версию приложения. Пробная, если пользователь загрузил пробную версию приложения.
Дата покупки Дата приобретения приложения.

Элемент ProductReceipt

Этот элемент содержит сведения о покупке из приложения для текущего приложения.

ProductReceipt содержит следующие атрибуты.

attribute Описание
Id Идентифицирует покупку.
Appid Определяет приложение, через которое пользователь совершил покупку.
ProductId Определяет приобретенный продукт.
Тип продукта Определяет тип продукта. В настоящее время поддерживает только значение Durable.
Дата покупки Дата покупки.