구글 in-app 결제 관련하여 알고계신 분들도 있지만 잘못 알려진 부분도 있는 것 같아 공유 드립니다.

 

Q. in-app 결제 영수증 검증 시스템은 구글에서 제공하는 것을 이용한다?

A. 아닙니다. 구글 결제 시스템은 google play를 이용하여 클라이언트와만 연동 됩니다실질적으로 아이템을 지급하는 게임서버와 구글 시스템과는 아무런 연동이 없습니다. 영수증의 인증은 게임 서버가 직접 해야 합니다.

 

Q. 그럼 결제 발생시 웹 콜은 왜 하나요? 

A. 해당 호출은 구글 시스템에 질의하는 것이 아닙니다. 서버가 C++등 언어 차원에서 인증 라이브러리를 제공하지 않는 경우 웹 서버를 두고 php와 같이 라이브러리 차원에서 인증을 지원하는 언어를 사용하는 경우가 많습니다. 그렇다고 그 웹 서버들이 구글에게 질의하는 것은 아니고 php의 경우 openssl_verify 라는 함수를 이용하여 0 또는 1만을 리턴하고 있습니다.

이전 포스팅 (C++로 구글 In-app 결제 검증 구현)은 해당 기능을 구현한 함수를 C++ 버젼으로 만든 것이며 이를 이용해 웹 서버를 따로 두는 비용, 질의간 발생하는 시간 및 트래픽 비용을 줄일 수 있습니다.

 

   참고 : Open Source PHP IAB Verification Library 

 

Q. 웹 콜을 이용해 지급요청 하는 상품과 영수증이 일치하는지 알수 있다?

A. 아닙니다. 웹 서버는 public key와 원문 데이터를 이용해 암호화 한 다음 주어진 signature와 동일한지만 검사합니다클라이언트에서 1달러 짜리 구매를 하고 발급 받은 영수증 데이터 게임 서버에 100달러 짜리 상품을 요청하며 제출 한다고 해도 웹서버에서는 정상적인 영수증이라면 ok를 리턴 합니다. 1달러 영수증인지 100달러 영수증인지를 검증하는 것은 게임 서버에서 해야 합니다.

 

Q. 웹 콜을 이용해 영수증이 재 사용되는지 검증 할 수 있다?

A. 아닙니다. 위에서 언급했다 싶이 웹 서버는 영수증의 정합성만을 검증합니다. google play에서 정상적으로 발급된 영수증이라면 몇 번이라도 ok를 리턴합니다. 영수증 재사용 여부 역시 게임 서버에서 검증해야 합니다.

 

Q. 게임 서버에서는 어떻게 상품과 영수증의 정합성, 재사용 여부를 알 수 있는가?

A. 2011 google IO 세션에서 소개된 방업은재 클라이언트에서 결제가 발생하기 전 서버로부터 임시값을 발급 받습니다(임시값은 1회성이며 유일성이 보장되어야 합니다). 이 임시값을 in-app결제 요청 json data의 developerPayload에 넣어 구매를 진행 합니다. developerPayload는 어플리케이션 개발자 임의로 지정할 수 있는 값으로써, 결제 결과 리턴시 그대로 리턴 되며 signature를 만들 때 데이터로 사용되므로 해당 데이터가 다르다면 정합성 인증에서 실패합니다. 게임 서버에서는 임시값에 대한 상품이 지급 되었는지 아닌지 체크하여 결제 성공 여부를 결정하면 됩니다. 


정상적인 상품과 영수증의 관계 또한 productId(구글 developer console에 등록) 항목을 이용해 검증 가능합니다. 게임 서버에서 productId와 지급 아이템의 매핑 테이블을 가지고 있다가 productId와 매칭 되는 아이템을 지급 해줘야 합니다.


 참고 : Android In-App Billing: Server Verification and Content Delivery 

          Google IO session video 

          Google IO session presentation slides

Posted by kukuta

댓글을 달아 주세요

  1. tolik 2013.11.20 06:45  댓글주소  수정/삭제  댓글쓰기

    Hi. How can i connect to you?

  2. tolik 2013.11.20 06:45  댓글주소  수정/삭제  댓글쓰기

    Hi. How can i connect to you?

다른 언어에서는 기본적인 verify 라이브러리들이 지원이 되는데 유독 C/C++에서는 관련 라이브러리가 없더군요. 
다행히 openssl이라는 라이브러리가 있어 그걸 사용하여 간단한 verify 함수를 만들어 보았습니다.
(코드는 몇줄 안되긴 하지만 샘플 코드라던지 자세한 설명이 없어 아래 코드를 만드는데 몇일 걸렸네요..;;)

본 포스트는 :
  • 구글 인-앱 결제를 어떻게 하는지 설명하지 않습니다. 결제 이후 나오는 signature를 검증하는 방법에 대해서 다룹니다.
    결제 방식을 알고 싶으시면 여기로->http://developer.android.com/google/play/billing/api.html#purchase)
  • Google Developer Console에 등록하는 방법, public_key를 얻는 방법, 상품을 등록하는 방법들에 대해 설명하지 않습니다.
  • C++ 언어 사용자를 대상으로 합니다

아래 코드를 읽기 위해선.. :
  • 샘플 코드의 signature, public_key, purchase_data는 당연히 가짜 입니다. 저대로 실행하면 0 리턴 합니다. 
  • 아래대로 빌드하면 Base64Decode 함수를 찾을 수 없다는 오류가 뜹니다. 적절한 코드 베끼시면 됩니다
  • 당연히 openssl 라이브러리 import 해야 합니다.
  • std::shared_ptr를 사용했으니 C++0x 혹은 C11 버젼 사용하셔야 합니다.
    (아니면 코드를 고치시면 됩니다. 각 변수들의 XXX_free 함수 보이시죠??)
  • 어차피 아래 샘플로는 빌드 안되는거 iostream 같은 자잘한 include는 당연히 알고 있다 생각하고 넘어갑니다.
  • OpenSSL_add_all_digests(), EVP_cleanup() 함수가 밖으로 빠져 있는 것에 주목해 주세요. 
    InappBillingVerify함수 안에 같이 들어가도 되지만 멀티 쓰레드에서 돌아가는 경우 에러를 발생 시킵니다. 저 두 함수는 thread-safe하지 않습니다.

Sample Code

#include <openssl/evp.h>
#include <openssl/pem.h>

int InappBillingVerify(const char* data, const char* signature, const char* pub_key_id)
{
    std::shared_ptr<EVP_MD_CTX> mdctx =
    std::shared_ptr<EVP_MD_CTX>(EVP_MD_CTX_create(), EVP_MD_CTX_destroy);
    const EVP_MD* md = EVP_get_digestbyname("SHA1");

    EVP_VerifyInit_ex(mdctx.get(), md, NULL);

    EVP_VerifyUpdate(mdctx.get(), (void*)data, strlen(data));

    std::shared_ptr<BIO> b64 = std::shared_ptr<BIO>(BIO_new(BIO_f_base64()), BIO_free);
    BIO_set_flags(b64.get(),BIO_FLAGS_BASE64_NO_NL);

    std::shared_ptr<BIO> bPubKey = std::shared_ptr<BIO>(BIO_new(BIO_s_mem()), BIO_free);
    BIO_puts(bPubKey.get(),pub_key_id);
    BIO_push(b64.get(), bPubKey.get());

    std::shared_ptr<EVP_PKEY> pubkey = 

std::shared_ptr<EVP_PKEY>(d2i_PUBKEY_bio(b64.get(), NULL), EVP_PKEY_free); std::string decoded_signature = Base64Decode(std::string(signature)); return EVP_VerifyFinal(mdctx.get(), (unsigned char*)decoded_signature.c_str(), 

decoded_signature.length(), pubkey.get()); } int main() { const char* purchase_data = "{" "\"orderId\":\"12999763169054705758.1371079406387615\"," "\"packageName\":\"com.example.app\"," "\"productId\":\"exampleSku\"," "\"purchaseTime\":1358227642000," "\"purchaseState\":0," "\"developerPayload\":\"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ\"," "\"purchaseToken\":\"rojeslcdyyiapnqcynkjyyjh\"" "}"; const char* public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAO..."; const char* signature = "DW0nEjsBhqHpECrS5Tcq6Y51vM..."; OpenSSL_add_all_digests(); std::cout << InappBillingVerify(purchase_data, signature, public_key) << std::endl; EVP_cleanup(); return 0; }

  • purchase_data

    • string in JSON format that is mapped to the INAPP_PURCHASE_DATA
    • 구글 인-앱 결제 완료 후 클라이언트가 서버로 보내온 결제 정보
    • packageName : 패키지 이름. 정상적인 app으로 부터 도착한 메시지인지 검증
    • product_id : 상품 아이디. 게임 서버에 상품아이디와 아이템을 매핑 시켜 놓고, 상품 아이디를 키로 삼아 아이템 지급
    • developerPayload : 개발시 임의로 넣는 값. 클라이언트 사이드에서 Google Play 구매 요청시 전송하면 결과 json에 포함되어 리턴. 매번 다른 값을 발급하여 인증 요청 중복 체크등에 사용
  • signature : Google Play 결제 결과로 리턴되는 암호화 된 값(RSA+SHA1, Base64 encoded, PKCS#1 padding)
  • public_key  
  • return
    • valid : 1
    • invalid : 0
    • error : -1

참고 : php 버젼. 결과가 이상하다 싶을 땐 이걸로 테스트 해보길 권장함.
https://github.com/mgoldsborough/google-play-in-app-billing-verification


Posted by kukuta

댓글을 달아 주세요

  1. 2013.11.18 15:57  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2013.11.21 10:35 신고  댓글주소  수정/삭제

      뻑 난다는것이..
      메모리 참조 오류 같은 걸로 코어가 떨어지고 죽어 버린다는 것인가요? 아니면 정상적으로 처리되어야 하는데 이상하게 처리 된다는 것인가요?

      아마도 프로세스가 죽었다는 의미 같긴한데..
      openssl 구조체나 문자열에 null 값이 있는것은 아닌가 합니다.

      1. 넘어온 문자열에 null이 있다.
      2. 샘플 코드에서 빠진 초기화 코드 같은 것이 있어서 openssl 구조체에 null이 들어 있다.

  2. tolik 2013.11.20 21:54  댓글주소  수정/삭제  댓글쓰기

    Where should i get orderId?

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2013.11.21 10:28 신고  댓글주소  수정/삭제

      after finishing the tranaction between 'google play' and your app.
      google play may return json structure.
      and it may contain the 'orderid'

    • tolik 2013.11.22 06:44  댓글주소  수정/삭제

      So its finish of transaction. Is there code in c++ of how should i initiate it? Thank you.

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2013.11.25 11:44 신고  댓글주소  수정/삭제

      No c++ code to get 'orderId'. I think you should use JAVA code provided by Google.

      You may find 'orderId' in 'INAPP_PURCHSE_DATA'(is element of INAPP_PURCHSE_DATA_LIST).
      http://developer.android.com/google/play/billing/api.html#purchase
      see above link. In 'Purchasing Items' section, attention 'getPurchases()' call. Through it, you can take 'INAPP_PURCHSE_DATA_LIST' and 'INAPP_PURCHSE_DATA'.

      see below link Table 4 : http://developer.android.com/google/play/billing/billing_reference.html
      'INAPP_PURCHSE_DATA' has orderId.

      sample code is blow :
      http://stackoverflow.com/questions/14262230/getting-order-id-when-querying-for-purchased-items

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2013.11.25 11:47 신고  댓글주소  수정/삭제

      if you have problem using JAVA, and should use C++.
      NDK may be the solution.

  3. youngi 2014.09.03 11:14  댓글주소  수정/삭제  댓글쓰기

    감사합니다.