使用收据验证产品购买Use receipts to verify product purchases

每个成功购买产品的 Microsoft Store 交易都可以选择返回交易收据。Each Microsoft Store transaction that results in a successful product purchase can optionally return a transaction receipt. 此收据向客户提供所列出的产品和货币成本的信息。This receipt provides information about the listed product and monetary cost to the customer.

有权访问此信息可支持以下方案:你的应用需要验证用户是否购买了你的应用,或者是否已从 Microsoft Store 进行了加载项(也称为应用内产品或 IAP)购买。Having access to this information supports scenarios where your app needs to verify that a user purchased your app, or has made add-on (also called in-app product or IAP) purchases from the Microsoft Store. 例如,请设想一个提供下载内容的游戏。For example, imagine a game that offers downloaded content. 如果购买了该游戏内容的用户要在其他设备上玩这个游戏,则需要验证该用户是否已经拥有此项内容。If the user who purchased the game content wants to play it on a different device, you need to verify that the user already owns the content. 操作方法如下。Here's how.

重要

此文章将介绍如何使用 Windows.ApplicationModel.Store 命名空间的成员来获取和验证应用内购买的收据。This article shows how to use members of the Windows.ApplicationModel.Store namespace to get and validate a receipt for an in-app purchase. 如果你使用 Windows.Services.Store 命名空间进行应用内购买(在 Windows 10 版本 1607 中引入,可在 Visual Studio 中用于面向 Windows 10 周年纪念版(10.0;版本 14393) 或更高版本的项目),此命名空间不会提供 API 用于获取应用内购买的购买收据。If you are using the Windows.Services.Store namespace for in-app purchases (introduced in Windows 10, version 1607, and available to projects that target Windows 10 Anniversary Edition (10.0; Build 14393) or a later release in Visual Studio), this namespace does not provide an API for getting purchase receipts for in-app purchases. 但是,你可以使用 Microsoft Store 集合 API 中的 REST 方法获取购买交易的数据。However, you can use a REST method in the Microsoft Store collection API to get data for a purchase transaction. 有关详细信息,请参阅应用内购买的收据For more information, see Receipts for in-app purchases.

索要收据Requesting a receipt

Windows.ApplicationModel.Store 命名空间支持多个方法来获取收据:The Windows.ApplicationModel.Store namespace supports several ways to get a receipt:

应用收据如下所示。An app receipt looks something like this.

备注

此示例的格式有助于提高 XML 的可读性。This example is formatted to help make the XML readable. 实际应用收据元素之间不包含空格。Real app receipts do not include whitespace between elements.

<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>

产品收据如下所示。A product receipt looks like this.

备注

此示例的格式有助于提高 XML 的可读性。This example is formatted to help make the XML readable. 实际产品收据元素之间不包含空格。Real product receipts do not include whitespace between elements.

<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>

你可以使用任一收据示例来测试自己的验证代码。You can use either of these receipt examples to test your validation code. 有关收据内容的详细信息,请参阅元素和属性说明For more information about the contents of the receipt, see the element and attribute descriptions.

验证收据Validating a receipt

若要验证收据的真实性,需要后端系统(Web 服务或类似系统)使用公钥证书检查收据的签名。To validate a receipt's authenticity, you need your back-end system (a web service or something similar) to check the receipt's signature using the public certificate. 若要获取此证书,请使用 URL https://go.microsoft.com/fwlink/p/?linkid=246509&cid=CertificateId,其中 CertificateId 是收据中的 CertificateId 值。To get this certificate, use the URL https://go.microsoft.com/fwlink/p/?linkid=246509&cid=CertificateId, where CertificateId is the CertificateId value in the receipt.

以下是该验证过程的一个示例。Here's an example of that validation process. 此代码在 .NET Framework 控制台应用程序中运行,该应用程序包含对 System.Security 程序集的引用。This code runs in a .NET Framework console application that includes a reference to the System.Security assembly.

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

收据的元素和属性说明Element and attribute descriptions for a receipt

本部分介绍收据的元素和属性。This section describes the elements and attributes in a receipt.

Receipt 元素Receipt element

此文件的根元素是 Receipt 元素,其中包含应用和应用内购买的信息。The root element of this file is the Receipt element, which contains information about app and in-app purchases. 此元素包含以下子元素。This element contains the following child elements.

元素Element 必需Required 数量Quantity 描述Description
AppReceiptAppReceipt No 0 或 10 or 1 包含当前应用的购买信息。Contains purchase information for the current app.
ProductReceiptProductReceipt No 0 或更多0 or more 包含有关当前应用的应用内购买的信息。Contains information about an in-app purchase for the current app.
签名Signature Yes 11 此元素是一种标准 XML-DSIG 构造This element is a standard XML-DSIG construct. 它包含 SignatureValue 元素(其中包含可用于验证收据的签名)和 SignedInfo 元素。It contains a SignatureValue element, which contains the signature you can use to validate the receipt, and a SignedInfo element.

Receipt 具有以下必属性。Receipt has the following attributes.

属性Attribute 描述Description
版本Version 收据的版本号。The version number of the receipt.
CertificateIdCertificateId 用于对收据进行签名的证书指纹。The certificate thumbprint used to sign the receipt.
ReceiptDateReceiptDate 收据签名和下载的日期。Date the receipt was signed and downloaded.
ReceiptDeviceIdReceiptDeviceId 标识用于请求此收据的设备。Identifies the device used to request this receipt.

AppReceipt 元素AppReceipt element

此元素包含当前应用的购买信息。This element contains purchase information for the current app.

AppReceipt 具有以下属性。AppReceipt has the following attributes.

属性Attribute 描述Description
IdId 标识购买。Identifies the purchase.
AppIdAppId 操作系统用于该应用的程序包系列名称值。The Package Family Name value that the OS uses for the app.
LicenseTypeLicenseType 完整,如果用户购买了完整版本的应用。Full, if the user purchased the full version of the app. 试用,如果用户下载了应用的试用版。Trial, if the user downloaded a trial version of the app.
PurchaseDatePurchaseDate 获得应用的日期。Date when the app was acquired.

ProductReceipt 元素ProductReceipt element

此元素包含有关当前应用的应用内购买信息。This element contains information about an in-app purchase for the current app.

ProductReceipt 具有以下属性。ProductReceipt has the following attributes.

属性Attribute 描述Description
IdId 标识购买。Identifies the purchase.
AppIdAppId 标识应用,用户通过该应用进行购买。Identifies the app through which the user made the purchase.
ProductIdProductId 标识购买的产品。Identifies the product purchased.
ProductTypeProductType 确定产品类型。Determines the product type. 当前仅支持值 DurableCurrently only supports a value of Durable.
PurchaseDatePurchaseDate 购买发生的日期。Date when the purchase occurred.