LoginSignup
10
12

More than 5 years have passed since last update.

C#でのInAppPurchaseレシート検証処理(App Storeを使用)

Last updated at Posted at 2015-01-05

はじめに

iPhoneでのInAppPurchase(消耗型課金)を実装しました。
StoreKitによる購入完了後、サーバ側のアイテム購入処理を呼び出しています。

その際、不正利用を防ぐためサーバ側処理内でレシートの正当性検証をしています。
C#で実装したその検証部分を共有します。

こちらの実装を元に改変を行っています。
https://github.com/Redth/APNS-Sharp

レシート情報自体はBase64で受け取った想定です。

レシートクラスとレシート検証処理クラス

レシートクラス

Apple側サーバのレスポンスからレシート情報を取り出しています。
以下の例ではoriginal_transaction_idとproduct_idのみ取得しています。

Receipt.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;

namespace [namespace]
{
    [Serializable()]
    public class Receipt
    {
        public class InApp
        {
            public string quantity { get; set; }
            public string product_id { get; set; }
            public string transaction_id { get; set; }
            public string original_transaction_id { get; set; }
            public string purchase_date { get; set; }
            public string purchase_date_ms { get; set; }
            public string purchase_date_pst { get; set; }
            public string original_purchase_date { get; set; }
            public string original_purchase_date_ms { get; set; }
            public string original_purchase_date_pst { get; set; }
            public string is_trial_period { get; set; }
        }

        public List<InApp> in_app { get; set; }

        public int Status { get; set; }

        #region Constructor

        /// <summary>
        /// Creates the receipt from Apple's Response
        /// </summary>
        /// <param name="receipt"></param>
        public Receipt(string receipt)
        {
            JObject json = JObject.Parse(receipt);

            int status = -1;

            int.TryParse(json["status"].ToString(), out status);
            this.Status = status;

            if (this.Status != 0)
            {
                throw new Exception();
            }

            json = (JObject)json["receipt"];

            in_app = new List<InApp>();
            foreach (var token in json["in_app"].Children())
            {
                InApp inapp = new InApp();
                inapp.original_transaction_id = (string)token["original_transaction_id"];
                inapp.product_id = (string)token["product_id"];
                in_app.Add(inapp);
            }
        }

        #endregion Constructor

    }
}

検証処理クラス

ReceiptVerification.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using [Receiptクラスのnamespace];

namespace [namespace]
{
    public class ReceiptVerification
    {
        #region Constants
        private const string urlSandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
        private const string urlProduction = "https://buy.itunes.apple.com/verifyReceipt";
        #endregion

        #region Public Static Methods
        /// <summary>
        /// Sends the ReceiptData to the Verification Url to be verified.
        /// </summary>
        /// <returns>If true, the Receipt Verification Server indicates a valid transaction response</returns>
        public static bool IsReceiptValid(Receipt receipt)
        {
            return (receipt != null && receipt.Status == 0);
        }

        public static Receipt GetReceipt(bool sandbox, string receiptData)
        {
            return GetReceipt(sandbox ? urlSandbox : urlProduction, receiptData);
        }

        public static Receipt GetReceipt(string url, string receiptData)
        {
            Receipt result = null;

            string post = PostRequest(url, ConvertReceiptToPost(receiptData));

            if (!string.IsNullOrEmpty(post))
            {
                try { result = new Receipt(post); }
                catch { result = null; }
            }

            return result;
        }
        #endregion

        #region Private Static Methods

        /// <summary>
        /// Make a string with the receipt encoded
        /// </summary>
        /// <param name="receipt"></param>
        /// <returns></returns>
        private static string ConvertReceiptToPost(string receipt)
        {
            return string.Format(@"{{""receipt-data"":""{0}""}}", receipt);
        }

        /// <summary>
        /// Sends a request to the server and reads the response
        /// </summary>
        /// <param name="url"></param>
        /// <param name="postData"></param>
        /// <returns></returns>
        private static string PostRequest(string url, string postData)
        {
            byte[] byteArray = Encoding.UTF8.GetBytes(postData);
            return PostRequest(url, byteArray);
        }

        /// <summary>
        /// Sends a request to the server and reads the response
        /// </summary>
        /// <param name="url"></param>
        /// <param name="byteArray"></param>
        /// <returns></returns>
        private static string PostRequest(string url, byte[] byteArray)
        {
            try
            {
                WebRequest request = HttpWebRequest.Create(url);
                request.Method = "POST";
                request.ContentLength = byteArray.Length;
                request.ContentType = "text/plain";

                using (System.IO.Stream dataStream = request.GetRequestStream())
                {
                    dataStream.Write(byteArray, 0, byteArray.Length);
                    dataStream.Close();
                }

                using (WebResponse r = request.GetResponse())
                {
                    using (System.IO.StreamReader sr = new System.IO.StreamReader(r.GetResponseStream()))
                    {
                        return sr.ReadToEnd();
                    }
                }
            }
            catch
            {
                return string.Empty;
            }
        }

        #endregion Private Static Methods
    }
}

呼び出し部分

先に本番環境に投げ、取得失敗の場合にSandbox環境に投げています。

サーバ側購入処理クラスのメソッド内にて
                        // レシートデータの正当性検証
                        // 先に本番環境に投げ、取得失敗の場合、Sandbox環境へ投げる
                        Receipt receipt = ReceiptVerification.GetReceipt(false, base64ReceiptDataString);

                        if (!ReceiptVerification.IsReceiptValid(receipt))
                        {
                            receipt = ReceiptVerification.GetReceipt(true, base64ReceiptDataString);
                        }

                        if (!ReceiptVerification.IsReceiptValid(receipt))
                        {
                            // 処理終了
                            return -1;
                        }

                        // 処理の続き...

おわりに

レシート正当性検証はAppleにレシート情報を送付することで実現しています。
Appleに送付せずに検証することも可能です。

その場合、こちらを参考にしてください。
https://developer.apple.com/jp/devcenter/ios/library/documentation/ValidateAppStoreReceipt.pdf

自分が実装する際に消耗型課金の実装例やC#でのサーバ側実装情報をなかなか見つけられなかったので、何かの参考になりましたら幸いです。

10
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
12