2016年3月16日 星期三

Google Play 驗證 In-app Billing 電子收據 - 實作篇

經過上一篇介紹如何進行 Google Play 驗證 In-app Billing  如何進行設定後,本篇將解說驗證的工作流程。
Google API 使用 OAuth 2.0 認證機制,在認證之前我們需要先產生 JWT 向 Google 要求一組 Token,
然後再將 Google 給的 Token 在 Call API 時一并放在 Request 內一起送過去。
什麼是 JWT呢?簡單來說它是一個開放標準基於 json 的一種認證方式可以在不同的網域間共享資訊。
想更進一步的了解 JWT可以到 http://self-issued.info/docs/draft-ietf-oauth-jwt-bearer.html

驗證的第一步就是先向 Google 要來一組 Token,往後的 API 呼叫都需要用到它,
附帶一提 Token 有時效性(印象中最高可設定1小時)

///
/// 向 Google 索取存取API所需的 Token
/// 
public static void FetchGoogleAccessToken() {
    //JOSE Header 設定使用何種加密方式進行簽章
    var header = new { typ = "JWT", alg = "RS256" };
    var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
    var issueTime = DateTime.UtcNow;
    var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
    var exp = (int)issueTime.AddMinutes(60).Subtract(utc0).TotalSeconds;
    //ClamsSet傳送到 Google 進行 Auth 時的資訊
    var claimset = new {
        iss = /*上一篇有提到的  EMAIL ADDRESS */,
        scope = "https://www.googleapis.com/auth/androidpublisher",
        aud = "https://accounts.google.com/o/oauth2/token",
        iat,
        exp
    };

    // Encoded header
    var headerSerialized = JsonConvert.SerializeObject(header);
    var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
    //網路上找的到 Base64UrlEncode 不貼碼了
    var headerEncoded = Base64UrlEncode(headerBytes);

    // Encoded claimset
    var claimsetSerialized = JsonConvert.SerializeObject(claimset);
    var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
    //網路上找的到 Base64UrlEncode 不貼碼了
    var claimsetEncoded = Base64UrlEncode(claimsetBytes);
    //C# 6.0 的語言 可替換成 string.Format("{0}.{1}",headerEncoded,claimsetEncoded)
    var input = $"{headerEncoded}.{claimsetEncoded}";
    var inputBytes = Encoding.UTF8.GetBytes(input);

    // signiture
    var certificate = new X509Certificate2(
        Path.Combine(
            /*存放P12檔的路徑*/,
            /*P12檔的檔名*/),
        "notasecret");
    var rsa = (RSACryptoServiceProvider)certificate.PrivateKey;
    var cspParam = new CspParameters {
        KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
        KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
    };
    var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
    var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
    var signatureEncoded = Base64UrlEncode(signatureBytes);

     //JWT 需要將三個連在一起︰Header,Claims Set 和 Sign 然後用 . 隔開
    var jwt = $"{headerEncoded}.{claimsetEncoded}.{signatureEncoded}";
    var r = (HttpWebRequest)WebRequest.Create("https://accounts.google.com/o/oauth2/token");
    r.Method = "POST";
    r.ContentType = "application/x-www-form-urlencoded";
    r.UserAgent = string.Format(Section.Get.Common.Culture, " Mozilla/4.0 (compatible; Win32; {0}.{1})", 1, 0);
    ServicePointManager.ServerCertificateValidationCallback = (sender, certificates, chain, sslPolicyErrors) => true;
    var postData = $@"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={jwt}";
    var postBytes = Encoding.UTF8.GetBytes(postData);
    r.ContentLength = postBytes.Length;
    using (var postStream = r.GetRequestStream()) {
        postStream.Write(postBytes, 0, postBytes.Length);
        postStream.Close();
    }
    Task.Factory.FromAsync(r.BeginGetResponse, r.EndGetResponse, null).ContinueWith(t => {
        try {
            var result = t.Result.GetResponseStream();
            if (null == result) return;
            using (var sr = new StreamReader(result, Encoding.UTF8)) {
                var token = sr.ReadToEnd();
                //GoogleToken 這個類別只有三個欄位 string access_token、string token_type、int expires_in
                var tokenObj = JsonConvert.DeserializeObject(token);
                //tokenObj.access_token 需要保存下來
                Log.Info($"NewAccessToken={tokenObj.access_token}");
                sr.Close();
            }
        }
        catch (Exception ex) {
            Log.Error(ex.Message, ex);
        }
    }, TaskContinuationOptions.ExecuteSynchronously);
}

最後,當玩家玩成儲值流程時先別急著將道具或代幣給玩家,先向 Google 問一下這位玩家剛剛是否有真的消費
實作的方式可以參考下面的連結進行實作
https://developers.google.com/android-publisher/api-ref/purchases/products/get#request

沒有留言: