RSA 키를 제대로하면 해결했지만, 지금까지 실패한 계정에 관해서는 큐를 소화 할 방법을 찾지 못하고 공장 출하 상태로 할 수밖에없는 모양 ...■ GooglePlayDeveloperConsole 계정 정보에서 "테스트 권한이있는 Gmail 계정"대상의 Gmail 계정을 등록하지 않았다.
등록하지 않았다 위에 등록해도 몇 시간은 반영되지 않았다 ...■ (추가) BundleIdentifer가 다른 빌드이었다 ...
Android에서 BundleIdentifer 서명과 다를 결제하려고했을 때 GooglePlay가내는 대화에서는
"이 버전은 청구 할 수 없습니다"인 문구가 나온다.
틀림없이 버전 않을까 생각했지만,이 메시지는 서명에 결함이있는 경우 등에서도 나오는 모양이므로주의.
(빌드 작업이 여러 관계에서 Identifer도 여러 있었기 때문에 일어난)위의 부분에 순차적으로 막혀 결국 과금 테스트가 성공했습니다.
결제 요청을 하는 순간 '항목을 찾을 수 없습니다.' 오류가 나오고 확인을 누르면 바로 제대로 된 인앱 제품 정보가 나오는 증상이 있습니다. 이 에러는 Google Play Billing Library에 있는 Dungeons Sample를 연동해서 나온 오류입니다.
Dungeons.java의 소스중 onClick 함수에서 Buy버튼을 클릭했을 때 mBillingService.requestPurchase를 처리하는 if, else if 문 문제입니다. 딱 보시면 mBillingService.requestPurchase 리턴값에 따라 한번 더 mBillingService.requestPurchase 처리 될 수 있는 구조입니다.
'이 버전의 애플리케이션에서는 Google Play를 통한 결제를 사용할 수 없습니다. 자세한 내용은 도움말 센터를 참조하세요.' 애플리케이션 오류가 발생할 수 있습니다. 확인을 누르면 아래와 같은 로그가 발생합니다.
MarketBillingService.sendResponseCode: Sending response RESULT_DEVELOPER_ERROR for request xxxxxxxxxxxxxx to org.xxxx.game.
Google Play 개발자 센터에 등록한 APK와 기기에서 개발 테스트 중인 App의 버젼이 달라서 생기는 문제라고 합니다. 하긴 지금 개발자 센터에는 Billing 퍼미션만 추가된 서명된 APK가 올라가 있고, 현재 이클립스로 개발 테스트 중인 App은 Dungeons Sample을 붙여서 연동 개발 테스트까지 하고 있으니 더 개발된 상태죠.
그래서 서명된 APK를 새로 만들어 개발자 센터에도 올리고 디바이스에 APK를 넣어서 ASTRO 파일 관리자로 폰에 설치를 합니다. 이클립스에서 실행하는 것은 안됩니다. 테스트는 약 1시간 정도 기다린 후 해보시기 바랍니다. 새로 올린 APK가 바로 갱신 안되는 듯 싶네요. 그리고 다시 실행 후 구입을 시도하면 잘 되거나 다른 에러가 발생합니다.
'요청하신 항목은 구매할 수 없습니다.' 구입할 수 없음 에러가 발생할 수 있습니다. 확인을 클릭 누르면 아래와 같은 로그가 발생합니다.
MarketBillingService.sendResponseCode: Sending response RESULT_ITEM_UNAVAILABLE for request xxxxxxxxxxxxxxxx to org.xxxx.game.
판매자 계정과 기기에 로그인 된 구글 계정이 같아서 생기는 것이라는 말도 있습니다만 저는 회사 개발자 계정으로 테스트 중이라 제 계정과 같을리는 없죠. 개발자 센터 -> 프로필 수정에 보면 테스트 계정이라는 항목이 있는데, 여기에 제 구글 계정을 넣고 저장을 한 후 해봤지만 역시나 안되는군요.
제 경우에는 추가 된 인앱 제품을 게시를 안해서 생긴 문제였습니다. 테스트 중인 인앱 제품을 '게시완료' 후 테스트 하니 결제 진행이 되네요. 물론 App은 '게시 안됨'이어도 됩니다.
Security Recommendation: It is highly recommended that you do not hard-code the exact public license key string value as provided by Google Play. Instead, you can construct the whole public license key string at runtime from substrings, or retrieve it from an encrypted store, before passing it to the constructor. This approach makes it more difficult for malicious third-parties to modify the public license key string in your APK file.
7.4 추가 정보 입력 후 위의 save버튼 옆 inactive 버튼을 눌러 active로 바꿔준다.
*Warning: It may take up to 2-3 hours after uploading the APK for Google Play to recognize your updated APK version. If you try to test your application before your uploaded APK is recognized by Google Play, your application will receive a ‘purchase cancelled’ response with an error message “This version of the application is not enabled for In-app Billing.”
Ch.2 구현
인앱 구매에서 일반적으로 필요한 시나리오는 다음과 같다.
(아래는 모두 셋업이 끝난 후, 즉 IabHelper.startSetup(OnIabSetupFinishedListener listener)의 콜백에서 result.isSuccess()가 true를 반환한 경우에 해야 한다.)
1. 내가 등록한 아이템의 정보(이름, 가격 등의 세부 정보)를 알아오기.
2. 구매 절차
일반적으로 OnIabSetupFinishedListener에서 위 1을 수행해 아이템의 이름과 가격을 가져오고 난 뒤 그 정보를 유저에게 보여준다. 유저가 구매 버튼을 누른 경우 위 2를 수행한다.
1. 아이템 확인
1. Ch.1 - 6의 startSetup메소드의 결과 연결이 수립된 후 불리는 OnIabSetupFinishedListener에서 아래와 같이 인앱제품의 세부사항을 요청할 수 있다.
인앱 제품의 sku(고유 아이디. 위 Ch.1 - 7.3 참고)를 리스트에 넣고 인벤토리의 정보를 요청한다.
List<String> additionalSkuList = new ArrayList<String>();
안녕하세요.. 답답합니다 네 답답합니다. v2에서 v3으로 바뀐지 오래된것같은데 왜 저는 이제야 v3을 적용한것일까요.. 인터넷에 정보도 잘 없는것같고.. ( 물론 검색하니 나오긴합니다. 제가 할줄몰라 그런건지.. 허허 ) 삽질하는분 없으셨으면 하는 바람에 정리한번 해봅니다.
( 사실 다음에 또 인앱 구현할 일 있을때 이거보고 기억 더듬으며 편히 하게위함.... 은 비밀 )
구글 API문서도 도움되었지만, 한국어로 설명되어있으면 좋을것 같은 생각에.. 한 자 적어봅니다.
우선 SDK매니저 들어가셔서
사진에 보이는 Google Play Billing Libary 를 설치합니다.
그럼 SDK - extra -google - play_billing 폴더가 생길겁니다.
안에 샘플코드도 들어있으니 참고하시면 됩니다. ( 사실 전 샘플코드 별로 도움 안되었어요.. ㅜㅜ )
Sample폴더 안에 TrivialDrive 폴더가 있으면 인앱 버전3 이 맞습니다.
src폴더에 있는 java파일을 본인 프로젝트로 복사해줍니다.
android폴더에있는 android.vending.billing 파일도 역시 복사하셔야 되며 이 파일은 패키지명을 수정하면 안됩니다.
ServiceConnection mServiceConn = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { mService = null; } @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = IInAppBillingService.Stub.asInterface(service); } };
추가,
onDestroy 에는
@Override public void onDestroy() { super.onDestroy(); if (mServiceConn != null) { unbindService(mServiceConn); } }
와 같이 해줍니다.
onCreate안에다가
bindService(new Intent("com.android.vending.billing.InAppBillingService.BIND"), mServiceConn, Context.BIND_AUTO_CREATE); String base64EncodedPublicKey = ""; (구글에서 발급받은 바이너리키를 입력해줍니다) mHelper = new IabHelper(this, base64EncodedPublicKey); mHelper.enableDebugLogging(true); mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() { public void onIabSetupFinished(IabResult result) { if (!result.isSuccess()) { // 구매오류처리 ( 토스트하나 띄우고 결제팝업 종료시키면 되겠습니다 ) } AlreadyPurchaseItems(); // AlreadyPurchaseItems(); 메서드는 구매목록을 초기화하는 메서드입니다. v3으로 넘어오면서 구매기록이 모두 남게 되는데 재구매 가능한 상품( 게임에서는 코인같은아이템은 ) 구매후 삭제해주어야 합니다. 이 메서드는 상품 구매전 혹은 후에 반드시 호출해야합니다. ( 재구매가 불가능한 1회성 아이템의경우 호출하면 안됩니다 ) } }); }
그리고 AlreadyPurchaseItems 메서드입니다.
public void AlreadyPurchaseItems() { try { Bundle ownedItems = mService.getPurchases(3, getPackageName(), "inapp", null); int response = ownedItems.getInt("RESPONSE_CODE"); if (response == 0) { ArrayList purchaseDataList = ownedItems .getStringArrayList("INAPP_PURCHASE_DATA_LIST"); String[] tokens = new String[purchaseDataList.size()]; for (int i = 0; i < purchaseDataList.size(); ++i) { String purchaseData = (String) purchaseDataList.get(i); JSONObject jo = new JSONObject(purchaseData); tokens[i] = jo.getString("purchaseToken"); // 여기서 tokens를 모두 컨슘 해주기 mService.consumePurchase(3, getPackageName(), tokens[i]); } } // 토큰을 모두 컨슘했으니 구매 메서드 처리 } catch (Exception e) { e.printStackTrace(); } }
// 구매메서드 입니다.
public void Buy(String id_item) { // Var.ind_item = index; try { Bundle buyIntentBundle = mService.getBuyIntent(3, getPackageName(), id_item, "inapp", "test"); PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT"); if (pendingIntent != null) { // startIntentSenderForResult(pendingIntent.getIntentSender(), 1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0)); mHelper.launchPurchaseFlow(this, getPackageName(), 1001, mPurchaseFinishedListener, "test"); // 위에 두줄 결제호출이 2가지가 있는데 위에것을 사용하면 결과가 onActivityResult 메서드로 가고, 밑에것을 사용하면 OnIabPurchaseFinishedListener 메서드로 갑니다. (참고하세요!) } else { // 결제가 막혔다면 } } catch (Exception e) { e.printStackTrace(); } }
결과처리 메서드 2가지 다 설명드리겠습니다
우선 1번 방법
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { System.out.println("requestCode : " + requestCode); System.out.println("resultCode : " + resultCode); if(requestCode == 1001) if (resultCode == RESULT_OK) { if (!mHelper.handleActivityResult(requestCode, resultCode, data)) { super.onActivityResult(requestCode, resultCode, data); int responseCode = data.getIntExtra("RESPONSE_CODE", 0); String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA"); String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE"); // 여기서 아이템 추가 해주시면 됩니다. // 만약 서버로 영수증 체크후에 아이템 추가한다면, 서버로 purchaseData , dataSignature 2개 보내시면 됩니다. } else { // 구매취소 처리 } }else{ // 구매취소 처리 } else{ // 구매취소 처리 } }
2번째 방법입니다.
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() { public void onIabPurchaseFinished(IabResult result, Purchase purchase) { // 여기서 아이템 추가 해주시면 됩니다. // 만약 서버로 영수증 체크후에 아이템 추가한다면, 서버로 purchase.getOriginalJson() , purchase.getSignature() 2개 보내시면 됩니다. } };
이렇게하면 인앱처리 끝납니다. 다른문의사항있으면 댓글남겨주시면 답변드릴수있도록할게요 ㅎㅎ
요청하시는분이 많아서 인앱부분 소스 올려드립니다. 참고하세요 ㅎ
=======================
=======================
=======================
출처: http://theeye.pe.kr/archives/2130
현재 글에서 설명되는 상품의 타입중에 관리되지 않는 제품 (Consumable/Unmanaged Product)는 언제부터였는지는 정확하지 않지만 현재 제거된 상태입니다. 이제는 모든 앱내 상품들은 Google Play에 의해 관리되게 됩니다. 기본적으로 모든 관리되는 제품은 구매가 성공적으로 이루어졌을 때 보유(Owned) 상태가 됩니다. 이 상태에서는 Google Play를 통해 같은 상품을 중복 구매할 수 없게 됩니다.
이 보유상태의 제품을 consumePurchase() 를 호출하여 소진(Consume)하여 다시한번 비보유(Unowned) 상태로 되돌릴 수 있습니다. 이 소진행위는 Google Play로 하여금 다시 해당 상품을 구매 가능한 상태로 되돌리며 이전의 구매 정보를 파기하게 됩니다.
현재 출시되고 있는 수많은 Android 어플리케이션이 앱내 구매(In-App Billing) 기능을 제공하고 있습니다. 이러한 구매 기능을 쉽게 접할 수 있는 어플리케이션중에 게임이 있는데요. 많은 게임들이 해킹의 피해를 입고 있고 특히 프리덤(Freedom)과 같은 결제 해킹 앱들에 의해 피해를 보는 경우가 제 생각보다 많다는것을 알았습니다. 이러한 결제 해킹 앱들의 경우 폰의 루팅이 필요한데요. 루팅폰을 사용중인 유저의 비중이 생각보다 많은것 같습니다. 이 문서는 이러한 해킹으로 부터 나의 수익을 지키는 방법에 대해 정리해 보았습니다. 먼저 Android에 대해 기술하고 다음은 iOS에 대해 또 글을 올리겠습니다. Google Play가 제공하는 In-App Billing 버전3를 기준으로 정리하였으며 사전 지식이 부족하실 경우 이전에 작성한 [Android In-App Billing 구현하기 (IAB Version 3)]를 먼저 읽어보시길 권장합니다.
In-App Billing 상품의 타입에 대해 알아보기
Android Developer Console에서 “인앱 상품” 메뉴에 들어가서 상품을 추가하게 되면 볼 수 있는 화면입니다. 여기서 3가지 타입을 제공하는것을 볼 수 있습니다. 하지만 IAB 버전3 API의 경우 관리되는 제품/구독 2가지의 타입을 제공한다고 생각하시면 됩니다. 관리되지 않는 제품을 선택할 때 다음과 같은 화면을 볼 수 있습니다.
뒤에서 좀 더 자세하게 설명을 드리겠지만 관리되는 제품과 관리되지 않는 제품은 IAB v3에서는 동일하게 관리되는 제품으로 취급됩니다. 하지만 관리되는 제품은 소진이 불가능한(Non-Consumable) 영원히 사용자에게 귀속되는 상품이며 관리되지 않는 제품은 소진이 가능한(Consumable) 상품으로 의미가 갈립니다. 소진이 불가능한 상품이라는 의미는 똑같은 상품을 두번 이상 구매할 수 없는것을 의미합니다. 소진이 가능한 상품의 경우 똑같은 상품을 계속해서 반복 구매하는 것이 가능합니다. 게임의 중간화폐(Currency)가 이경우에 해당될것 같습니다. 구독의 경우에는 다음과 같이 한달 또는 일년 단위로 자동 연장되는 결제 방식을 의미합니다. 음원 서비스들에서 주로 볼 수 있는 상품의 모습이라고 생각됩니다. 구독의 취소는 [Google 월렛의 내 구독 정보]에서 취소할 수 있습니다.
상품을 추가하실때 위와 같은 화면을 볼 수 있습니다. 정리해 보자면 Android 에서 제공하는 상품의 종류는 크게 “관리되는 제품”과 “구독” 2가지를 제공하며 “관리되는 제품”에는 반복 구매할 수 없는 귀속되는 소진 불가 상품과 반복구매가 가능한 소진 가능 상품이 존재합니다.
여기서 굉장히 중요한것 한가지는 “관리되는 제품 – 소진불가” 상품은 구글측에서 구매 내역을 신뢰할 수 있는 수준에서 관리해 준다는것입니다. “구독”역시 마찬가지입니다. 하지만 “관리되는 제품 – 소진 가능”한 상품은 개발사에서 구매 내역을 직접 관리해야 합니다.
유저로부터 발생하는 CS중 “결제를 분명히 했는데 아이템이 들어오지 않았다”는 대부분 이 “관리되는 제품 – 소진가능”한 상품들의 구매 과정에서 발생합니다.
In-App Billing 버전3 결제 과정
먼저 중요하게 봐야 하는 Google In-App Billing 플로우입니다. isBillingSupported() 메소드 호출을 통해 앱내 결제가 가능한지 확인합니다. 디바이스와 OS버전의 상황을 체크하게 되는데 한국의 경우 여기서 실패하는 경우는 없다고 보시면 될것 같습니다.
다음은 getPurchases() 메소드입니다. 이 메소드는 현재 유저가 (이미 구매하여) 소유하고 있는 상품들의 정보를 반환합니다. 결제 플로우인데 왜 갑자기 쌩뚱맞게 이것을 호출하는가 의구심이 들 수 있습니다. 여기서 이 메소드를 호출함으로써 다음과 같은 정보를 얻을 수 있습니다.
유저가 기존에 구매한 “관리되는 상품 – 소진 불가” 상품의 리스트
유저가 기존에 구매한 “관리되는 상품 – 소진 가능” 상품중 아직 소진되지 않은 상품의 리스트
유저의 구독 상품의 리스트
결론부터 말씀드리자면 이 메소드는 앱의 구동시점에 호출해줄 필요가 있습니다. 앱의 구동 시점 또는 로그인 기반의 경우 로그인이 성공하는 시점(카카오 로그인 성공 등)에 이 메소드를 호출함으로써 유저가 기존에 구매한 상품들의 정보를 불러와 앱에 세팅할 수 있습니다.
그렇다면 “우리 회사는 유저가 구매한 아이템의 모든 정보를 우리 서버에 직접 저장하고 관리하고 있다. 그럼 이것을 호출할 필요가 없는가?” 라고 반문하실 수 있습니다. 제가 알기로는 대부분의 한국의 게임사들은 직접 서버를 보유하고 있고 구매한 상품의 정보를 서버에 직접 보관하시는것으로 알고 있습니다. 그렇다면 이 호출을 건너뛰셔도 상관없습니다. 하지만 그럼에도 불구하고 호출하셔야 하는 이유는 뒤에 좀 더 자세히 설명하겠지만 “관리되는 상품 – 소진 가능”한 상품의 경우 구매 직후 바로 소진을 하게 되는데요(게임내에 통용되는 화폐로 교환), 구매는 성공했는데 유저에게 상품을 지급하기 전에 오류로 인해 앱이 죽는다거나 하는 문제로 소진을 못하는 경우가 발생할 수 있습니다.
이러한 상품들의 경우에도 getPurchases()를 통해 정보를 받아오실 수 있습니다. 결제는 성공했지만 지급에는 실패한 상품의 경우 바로 지급을 처리해주시면 됩니다. 가령 상품 구매가 성공할 때 “500골드의 구매가 정상적으로 처리되었습니다” 라는 팝업을 띄우게 되어있다고 가정해 봅시다. 유저가 결제를 정상적으로 성공한 시점에 앱이 어떤 문제로 죽었습니다. 유저는 깜짝 놀라 앱을 다시 구동할 것입니다. 그리고 앱이 켜지자 마자 진행중이던 결제처리를 마저 진행하고 해당 팝업을 띄우시면 됩니다.
기존의 IAB v2의 결제직후 아이템 지급까지 안정성이 보장되지 않는 상황을 대처하기 위해 v3에서는 결제후 아이템의 지급시점까지 결제 내역을 관리해주도록 변경되었습니다. (심지어 “관리되지 않는 상품”조차도 소진시점까지 관리되는 상품으로써 관리를 해줍니다. 이말은 소진하기 전까지는 관리되는 상품과 동일하게 중복 구매가 불가능하다는것을 의미합니다.)
즉, “관리되는 상품 – 소진 불가”과 “구독”의 경우 결제가 성공한 시점부터 언제든지 getPurchases()를 호출하여 구매내역을 꺼내볼 수 있습니다. 하지만 “관리되는 상품 – 소진가능” 상품의 경우 결제 → 소진(지급)을 거쳐서 처리하도록 되어있습니다. 이 소진을 하기 전까지는 “관리되는 상품 – 소진가능”(관리되지 않는 제품)일지라도 Google이 관리해줍니다. 소진에 대해서는 뒤에서 또 이야기 하기로 하고 계속해서 플로우를 설명해 보겠습니다.
getSkuDetails()는 판매 가능한 상품들의 상세 정보를 리스트로 반환합니다. 여기서 중요한점은 기존에 Google Play에 정의해둔 상품의 ID들을 모두 알고 있어야 하며 이 ID들을 이용하여 메소드를 호출하게 됩니다. 이 메소드를 호출하여 얻을 수 있는 정보는 상품의 가격, 이름, 설명, 구매 타입이 있습니다.
유저가 보유하고 있는 상품의 경우 getBuyIntent()를 사용하여 구매를 진행할 수 있습니다. 이 메소드를 호출하기 위해서는 기존에 Google Developer Console에 정의해두었던 상품의 ID와 다른 추가적인 파라미터가 사용됩니다. 이후의 결제 진행은 다음과 같은 순서로 이루어집니다.
getBuyIntent()를 호출하면 Google Play는 구글 체크아웃 결제창을 시작할 수 있는 PendingIntent를 포함한 Bundle을 반환합니다.
당신의 어플리케이션에서 startIntentSenderForResult를 이용하여 위의 PendingIntent를 실행합니다.
체크아웃 결제가 종료된다면 (성공적으로 결제가 되었던지 유저가 결제를 중도에 취소하였던지) Google Play는 결과를 담은 Intent를 onActivityResult 메소드로 보내줍니다. 결과 코드를 통해 구매가 성공적으로 진행되었는지 취소되었는지 여부를 확인할 수 있습니다. 응답 Intent에는 구매 트랜젝션을 식별하는데 사용가능한 유니크한 purchaseToken을 포함한 구매한 상품의 정보를 담고 있습니다.
이번에는 소진에 대해 알아보겠습니다. Google Play를 통해 판매할 수 있는 앱내 상품중에 유일하게 “관리되는 상품 – 소진가능 (관리되지 않는 상품)”만이 이 소진 메소드인 consumePurchase()를 사용합니다.
좀 더 정리하여 보면 IAB v3에서는 모든 앱내 상품이 관리됩니다. Google Developer Console에는 “관리되지 않는 상품”이라고 표시되지만 실제로는 관리됩니다. 다만 이 “관리되지 않는 상품”은 소진을 하기 전까지만 관리됩니다. 즉, 상품을 구매하여 소진하기 전까지 Google은 이 상품을 유저가 소유하였다고 직접 관리하게 됩니다. 이미 소유한 상품은 두번 구매할 수 없습니다. 그리고 명시적으로 소진을 하게 되면 이후에 다시 이 상품은 소유하지 않은 상태가 되고 재구매가 가능하게 됩니다.
여기서 말하는 소진은 게임에서는 게임내 화폐로의 교환을 의미합니다. 좀 더 쉽게 설명해 보자면 게임내에서 500골드를 1,000원에 판매하고 있었다면 결제를 통해서는 500골드 교환권을 구매한것이 됩니다. 이 500골드 교환권은 실제 500골드로 교환할때까지 Google이 관리해줍니다. 그리고 모든 상품들에 대해 똑같은 교환권을 2개 이상 가지는것은 불가능합니다. 500골드 교환권을 500골드로 교환해야 다시 500골드 교환권을 구매할 수 있습니다.
정리
너무 길게 설명한것 같은데 정리해 보면 각각의 상품들은 다음과 같은 과정을 거처 구매가 진행됩니다.
상품 타입
V3에서의 의미
결제 플로우
관리되는 제품
관리되는 제품 – 소진 불가 (Non-Consumable)
getSkuDetails() → getBuyIntent() → startIntentSenderForResult() → 상품 지급
관리되지 않는 제품
관리되는 제품 – 소진 가능 (Consumable)
getSkuDetails() → getBuyIntent() → startIntentSenderForResult() → consumePurchase() → 상품 지급
구독
관리되는 제품 – 특정 기간동안 보유 (자동 결제)
getSkuDetails() → getBuyIntent() → startIntentSenderForResult() → 구독 시작
국내의 대부분의 게임이 결제를 통해 게임내에서 사용가능한 중간 화폐를 구매하는 모습을 차용하고 있다는것을 볼때 위의 3가지중에서 가장 중요한부분은 “관리되지 않는 제품”입니다. 소진을 하는 시점부터 Google이 관리를 해주지 않기 때문에 보안에 가장 취약한 상품 타입이기도 합니다.
결제 보안 시작하기
안드로이드 결제 보안을 설명하는데에는 구글이 IAB v3 예제로 제공하는 TriviaDrive 프로젝트를 이용하는것이 가장 좋다고 생각합니다. 해당 샘플 프로젝트를 Gradle 프로젝트로 컨버팅 하여 [이곳]에 올려두었으니 참고하시기 바랍니다. 프로젝트의 설명은 MainActivity의 코드를 가지고 해보겠습니다.
지금부터는 상품의 타입을 Google Developer Console에서 볼 수 있는 “관리되는 제품”, “관리되지 않는 제품”, “구독” 3가지로 명명하도록 하겠습니다. “관리되지 않는 제품”일지라도 소진 전까지는 관리가 된다는것을 유념해 주시기 바랍니다.
IAB v3를 이용하도록 만들어진 TriviaDrive 샘플 프로젝트는 3가지 타입의 상품을 구매하는 모든 로직이 포함되어있는 프로젝트입니다. 부가적인 기능들이 모두 잘 구현되어있는 예제이므로 개발중인 프로젝트에서 결제를 구현하실때는 이 프로젝트의 aidl 파일뿐만 아니라 util 디렉토리 이하의 모든 Java소스도 복사해서 사용하시기 바랍니다.
이 프로젝트는 단순한 운전 게임을 모티브로 만들어진 예제이며 자동차는 가스로 움직이며 가스를 저장하는 저장 탱크가 있습니다. 플레이어가 가스를 구매할때마다 탱크의 가스가 1/4씩 충전이 됩니다. 플레이어가 운전을 시작하면 이 가스가 점차 감소됩니다. (물론 이건 게임이니깐 한번에 1/4씩 감소됩니다)
플레이어는 “프리미엄 업그레이드”를 할 수 있습니다. 이 업그레이드를 하게 되면 유저는 기본적으로 부여되는 파란차가 아닌 빨간 차를 부여받게 됩니다.
마지막으로 플레이어는 “무한 가스” 상품을 구독할 수 있습니다. 이 상품을 구독하게 되면 구독하는 동안은 가스를 사용하지 않고 달릴 수 있게 됩니다.
각 상품의 소진 메커니즘에 대해 정리해보자면 다음과 같습니다.
프리미엄 업그레이드 : 이 상품은 소진을 하지 않습니다. 구매를 하는 시점부터 유저에게 귀속되며 이후 유저는 영원히 파란차 대신에 빨간차를 소유할 수 있게 됩니다.
무한 가스 : 이 상품은 “구독” 상품입니다. 마찬가지로 “구독” 상품은 소진되지 않습니다.
가스 : 가스를 구매하는 순간 상품은 유저에게 귀속됩니다. 그리고 이것을 소진함으로써 당신의 어플리케이션에 적용할 수 있습니다. 이 예제에서는 가스 탱크를 1/4 채우는 것을 의미합니다. 실제로는 구매 직후에 바로 가스탱크가 채워질것입니다. 이 시점에서 가스 상품은 소진되고 그로인한 영향이 당신의 게임에 영향을 끼치게 됩니다. 예를 들면 다음과 같은 시나리오로 동작합니다.
상태
설명
구매 전
가스 탱크에 가스가 1/2 차 있음
구매 진행중
가스 탱크에 가스가 1/2 차 있고 “가스” 상품을 보유함
구매 직후
“가스” 상품을 소진함, 가스 탱크에 가스가 3/4가 됨
구매 완료 후
가스 탱크에 가스가 3/4 차 있고 “가스” 상품을 더이상 보유하고 있지 않음
여기서 알아두어야 하는 다른 중요한 점은 유저가 “가스”상품을 구매하였지만 그것을 소진하기 전에 어플리케이션이 크래시 나거나 또는 다른 어떤 일이 발생할 경우입니다. 그래서 우리는 게임이 시작될 때 유저가 “가스”아이템을 보유하고 있는지를 확인할 것입니다. 만약 보유중이라면 그것을 바로 소진하고 게임상의 가스 탱크를 채울것입니다. 이것은 매우 중요한 부분입니다.
테스트를 위해 위와 같이 상품을 등록하였습니다. 실제로 소스상에서는 다음과 같이 상품의 ID를 미리 정의해 두었습니다.
1
2
3
4
5
6
// 판매되는 상품 : premium (Non-Consumable), gas (Consumable)
여기서 유심히 봐야 하는 부분은 base64EncodedPublicKey라는 변수입니다. 이 값은 앱마다 다른 공개키 이며 Google Developer Console에서 확인이 가능합니다.
위의 값을 넣어주시면 됩니다. 이 값을 이용하여 구매가 성공하였을 때 Google Play로 부터 넘겨받은 데이터가 변조되었는지 여부를 검증할 수 있습니다.
보안팁 : 이 키는 공개키로써 이 키 자체가 어떤 비밀 정보를 담고 있지는 않습니다. 하지만 공격자가 이 키값을 손쉽게 위변조 하는것을 원치 않으므로 약간의 불폄함을 제공하는것도 방법입니다. 위의 코드를 게임내의 코드상에 직접 포함하는것보다는 조금 꼬아놓은 데이터를 포함시킨 뒤에 게임이 구동하는 시점에 조작을 통해 풀어서 사용하는것을 권장합니다. (가령 XOR 연산) 자체 서버가 있다면 XOR 연산에 사용할 키를 서버로부터 전송받는것도 방법일 것입니다. 어떤 방법을 사용해서 이부분은 완벽한 보안을 이루긴 어렵겠지만 공격자를 귀찮게 한다는 것에 의의가 있습니다.
Log.d(TAG,"Initial inventory query finished; enabling main UI.");
}
};
앱이 구동되지마자 IabHelper를 초기화 하고 가장먼저 호출한 코드입니다. 구매 내역을 가져와서 “프리미엄 업그레이드” 여부를 세팅하고 “무한 가스” 플랜을 구독중인지 여부를 게임내에 세팅하였습니다. 마지막으로 아직 소진되지 않은 “가스”가 존재하는지를 확인하여 소진시켜주는 과정입니다.
제작중인 게임 역시 유저의 결제는 정상적으로 이루어졌지만 어떤 오류로 인해 실제 지급까지 이루어지지 않은 경우에 위와 같은 과정을 통해 앱이 재실행되는 시점에 즉시 지급할 수 있습니다. 여기서 verifyDeveloperPayload 메소드가 매우 중요한데 뒤에서 다시 설명해보겠습니다.
각 상품들의 구매 호출시에 실행되는 코드입니다. 각 호출마다 마지막에 payload를 붙이는것을 확인 하실 수 있습니다.
보안팁 : 구매시에 사용되는 이 payload라는 값은 개발자가 지정해주는 임의의 문자열입니다. 여기에 넘겨주는 값이 결제 결과에 다시 그대로 담겨오게 됩니다. 즉 이 값이 구매 직전과 직후에 변동이 있다면 구매 요청에 변조가 있었다고 판단하시면 됩니다. “관리되지 않는 제품”의 경우에도 소진을 하기전까지는 관리가 되므로 소진을 하기전에 변조가 되었는지 여부를 확인하시는 로직을 추가하는것이 중요합니다. 이 예제에서는 verifyDeveloperPayload() 메소드가 그 역할을 하고 있습니다. 소진을 하기전에 게임이 크래시가 날수 있으므로 SharedPreferences등을 활용하여 Persistent하게 저장해두고 진행하는것을 추천합니다. 또는 서버에 developerPayload값을 전달하여 저장하게 하거나 혹은 아예 developerPayload값을 서버로부터 발급받는것도 방법입니다. 이후에 소진하는 시점에 SharedPreferences를 통해 저장한 값을 꺼내쓰거나 서버에 다시 질의하여 사용자가 최종 구매하였던 구매가 완료되지 못한 상품의 developerPayload를 받아오는 식으로 구현하면 될것입니다.
즉 위와같은 플로우가 될것입니다. 상품의 지급과 소진은 동시에 한시점에 이루어져야 하겠지만 굳이 순서를 정하자면 상품의 소진 이후에 상품을 지급하시기 바랍니다. 만약에 최악의 경우 소진이 이루어지는 직후에 게임이 크래시 난다면 이 부분은 CS에서 처리를 해야 할것입니다.
하지만 수많은 게임이 상품의 지급 정보를 서버에서 수행하므로 (보유 화폐의 증가 등) 원격 서버에서 developerPayload값을 검증을 위해 꺼내간 뒤에 지급이 이루어지지 않은 상태(로그 확인등)에서 유저의 CS가 들어온다면 해당분만큼을 추가 지급처리(보상) 해주면 될것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
booleanverifyDeveloperPayload(Purchasep){
Stringpayload=p.getDeveloperPayload();
/*
* TODO: 위의 그림에서 설명하였듯이 로컬 저장소 또는 원격지로부터 미리 저장해둔 developerPayload값을 꺼내 변조되지 않았는지 여부를 확인합니다.
*
* 이 payload의 값은 구매가 시작될 때 랜덤한 문자열을 생성하는것은 충분히 좋은 접근입니다.
* 하지만 두개의 디바이스를 가진 유저가 하나의 디바이스에서 결제를 하고 다른 디바이스에서 검증을 하는 경우가 발생할 수 있습니다.
* 이 경우 검증을 실패하게 될것입니다. 그러므로 개발시에 다음의 상황을 고려하여야 합니다.
*
* 1. 두명의 유저가 같은 아이템을 구매할 때, payload는 같은 아이템일지라도 달라야 합니다.
* 두명의 유저간 구매가 이어져서는 안됩니다.
*
* 2. payload는 앱을 두대를 사용하는 유저의 경우에도 정상적으로 동작할 수 있어야 합니다.
* 이 payload값을 저장하고 검증할 수 있는 자체적인 서버를 구축하는것을 권장합니다.
*/
returntrue;
}
이 이야기를 결론을 내보자면 궁극적으로 developerPayload의 발급과 검증을 모두 개발사가 보유한 서버에서 진행하는것을 추천합니다. 계속해서 코드 설명을 진행하겠습니다.
alert("You filled 1/4 tank. Your tank is now "+String.valueOf(mTank)+"/4 full!");
}
else{
complain("Error while consuming: "+result);
}
updateUi();
setWaitScreen(false);
Log.d(TAG,"End consumption flow.");
}
};
이 코드는 게임이 최초 실행될 때 실행되었던 queryInventoryAsync() 내에서도 소진할 상품이 있다면 호출되는 코드입니다. 결과를 확인하여 정상적으로 소진이 되었다면 해당 상품의 결과를 적용하면 됩니다.
보안팁 : 이 예제는 게임 클라이언트상에서 상품의 효과를 곧바로 적용하는것을 볼 수 있습니다. 하지만 이러한 상품의 지급은 서버에서 처리하는 것을 권장합니다. developerPayload를 검증하는 시점에 구매한 상품의 정보를 서버에 저장한 뒤에 소진이 성공하면 그 저장된 상품의 정보에 따라 해당 유저의 데이터베이스에 지급을 합니다. 지급이 성공적으로 이루어지면 클라이언트는 다시 서버로부터 최신 데이터를 가져와 화면의 정보를 갱신하도록 구현하면 됩니다.
중요 보안팁 : Google 에서는 상품의 구매 내역을 조회할 수 있는 서버에서 호출 가능한 API를 제공합니다. 패키지명, 상품 아이디, 구매시에 결과로 받았던 구매 토큰을 이용하여 유효한 구매인지를 Google측 서버에 직접 검증할 수 있습니다. 이 API를 사용하여 아이템 지급 전에 실제 유효한 구매였는지를 확인하는것만으로도 해킹의 대부분을 차단할 수 있습니다.
결과적으로 위와 같은 플로우가 됩니다. 여기서 중요한 핵심은 Google이 제공하는 앱내결제 체크 API를 사용하라는것과 아이템의 실제 지급을 서버에서 처리하라는것입니다. developerPayload의 경우 서버에서 3단계로 확인하는데요, 최초에는 발급하여 서버에 저장만을 하고 두번째에는 정상적으로 검증되었는지 여부를 저장합니다. 마지막 상품 지급 요청에 대하여 developerPayload가 존재하고 검증까지 마친상태인지를 확인하여 맞을 경우 다음 플로우로의 진행을 하게 됩니다. 만약에 developerPayload가 발급된적이 없거나 검증결과가 기록되지 않았다면 불법적인 지급 요청이겠죠.