Laravel 앱을 백앤드로 사용하는 안드로이드 프로젝트에서, 인앱결제 후 결제정보를 백앤드 API 을 이용하여 시스템에 반영하고 있었는데, 어느새 해당 API 를 임의로 호출하는 해킹 시도가 발생했다. 인앱 결제의 유효성 검증 단계 없이 저장하는 이런 보안에 취약한 API 를 위하여, 영수증 토큰값을 구글 API 를 통해 결제가 유효한지 검사하는 검증 로직과, 정기적으로 환불/주문취소/지불거절을 통해 무효화된 과거 인앱 주문에 대한 관리 로직을 추가했는데, 이번 포스팅에서 그 내용을 살펴본다.
아래 링크로 이동 하면 구글 개발자 콘솔상에서 해야할 내용들을 찾아볼 수 있다. (지만, 내용이 많고 장황한 설명이 제공되어, 원하는 내용을 한눈에 확인하기는 쉽지 않다.)
https://developers.google.com/android-publisher/getting_started?hl=ko#setting_up_api_access_clients

개발자 콘솔에 설정 > API 액세스 메뉴에서, 이미 등록된 OAuth 클라이언트와 서비스 계정이 없다면, 새롭게 만든다. 기등록된 서비스 계정이 있더라도, 해당 계정에 재무정보를 볼수 있는 권한이 없다면 해당 권한을 추가한다.
콘솔상에서, 서비스 계정을 생성시 다운받은 JSON credentials 파일은 서버의 적당한 위치에 복사하도록 한다. (본인의 경우 S3 에 저장한 다음, deploy 시에 base_path() 위치로 복사하는 방법을 사용했다.)
이제 구글 Play API 를 통하여 정상적인 결제 요청인지를 검증하는 API 와, 취소된 결제 리스트를 받아오는 API 를 위한 핸들러를 만들어야 하는데, 구글에서 제공하는 아래 패키지를 추가하면, OAuth 토큰 관리 등에 관한 로직을 직접 만들지 않아도 되므로, 간단히 작성할 수 있다.
composer require google/apiclient
위 패키지를 설치 후, 작성한 핸들러의 내용은 아래와 같다.
<?php namespace App\Services; use Exception; use Google_Client; use Google_Exception; use Google_Service_AndroidPublisher; class GooglePaymentHandler { private $client; private $service; public function __construct(Google_Client $client) { $this->client = $client; $this->client->addScope(['https://www.googleapis.com/auth/androidpublisher']); $this->client->setAuthConfig(base_path() . '/google.credentials.json'); $this->client->setIncludeGrantedScopes(true); } /** * 결제 검증 API * @param string $appName, (e.g. `개나소나`) * @param string $productCode, 안드로이드 상품코드 (e.g. `com.whatever.history.06`) * @param string $purchaseToken, 안드로이드 결제 영수증 코드 * @return object|null * @throws Google_Exception */ public function verify(string $appName, string $productCode, string $purchaseToken): ?object { $this->client->setApplicationName = $appName; $this->service = new Google_Service_AndroidPublisher($this->client); return $this->service->purchases_products->get( $this->getPackageName($appName), // com.some.thing $productCode, $purchaseToken ); } /** * 취소한 결제 리스트 API * @param string $appName * @return object|null * @throws Exception */ public function voidedPurchases(string $appName): ?object { $this->client->setApplicationName = $appName; $this->service = new Google_Service_AndroidPublisher($this->client); return $this->service->purchases_voidedpurchases->listPurchasesVoidedpurchases( $this->getPackageName($appName) // com.some.thing ); } /** * convert app-name to package-name (com.some.thing) * * @param string $appName * @return string|null */ private function getPackageName(string $appName): ?string { if ($appName === '개나소나') { return 'com.some.thing.xxx'; } if ($appName === '돈크라이') { return 'com.some.thing.yyy'; } if ($appName === '함흥차사') { return 'com.some.thing.zzz'; } return null; } }
각 API 로부터 리턴되는 객체에 대한 설명은 아래의 링크에서 볼 수 있다.
https://developers.google.com/android-publisher/api-ref/purchases/products/get
https://developers.google.com/android-publisher/api-ref/purchases/voidedpurchases/list
일반적이라면, verify() API 의 경우, 매 결제 요청시마다 호출하여, 그 validity 를 검사하도록 만들고, voidedPurchases() API 의 경우, CRON scheduler 로 등록한 Console Command 로 새벽시간에 1번 호출되도록 하여, 무효화된 결제에 관련된 사용자 구매 상태를 보정하도록 하면 되겠다.
Happy Coding!