はじめに
iPhoneでのInAppPurchase(消耗型課金)を実装しました。
StoreKitによる購入完了後、サーバ側のアイテム購入処理を呼び出しています。
その際、不正利用を防ぐためサーバ側処理内でレシートの正当性検証をしています。
C#で実装したその検証部分を共有します。
こちらの実装を元に改変を行っています。
https://github.com/Redth/APNS-Sharp
レシート情報自体はBase64で受け取った想定です。
レシートクラスとレシート検証処理クラス
レシートクラス
Apple側サーバのレスポンスからレシート情報を取り出しています。
以下の例ではoriginal_transaction_idとproduct_idのみ取得しています。
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
}
}
検証処理クラス
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#でのサーバ側実装情報をなかなか見つけられなかったので、何かの参考になりましたら幸いです。