안경잡이개발자

728x90
반응형

  일반적으로 안드로이드 어플리케이션을 개발할 때에는 인 앱 결제(IAP) 기능을 구현해야 할 때가 많습니다. 다만 이러한 인 앱 결제 기능을 구현할 때 알아 두어야 할 점이 있습니다. 그것은 바로 클라이언트가 제대로 카드 결제를 수행했는지의 여부입니다. 실제로 카드 결제를 하지 않고, 서버로 API만 호출하는 방식의 해킹 기법이 존재하기 때문입니다.

 

  따라서 서버는 클라이언트로부터 상품 ID(Product ID)와 구매 토큰(Purchase Token)을 전달 받도록 개발을 해야 합니다. 그래서 그 구매 토큰이 유효한지 여부를 확인하여, 구매 토큰이 유효할 때만 해당 구매자의 캐시를 증가시키는 등의 작업을 수행해야 합니다. 구매 토큰은 실제로 결제 처리를 했을 때 클라이언트가 발급 받을 수 있으므로, 서버 측에서 이러한 구매 토큰을 이용해 구글 구매 보고서 API에 접근해 해당 사용자의 결제가 사실인지 확인하는 것입니다.

 

  구매 토큰이 유효한지 여부를 확인하는 과정이 없으면, 실제로 카드 결제를 하지 않은 악성 사용자가 의도적으로 API를 호출하여 부당한 이득을 취할 수 있으므로 유의하셔야 합니다.

 

  결과적으로, 서버 API를 개발할 때는 구글 개발자 권한으로 결제 보고서에 접근할 수 있어야 합니다. 이를 위해 결제 보고서를 가져오는 Google API를 이용합니다. 따라서 사전에 개발자 콘솔에서 설정을 해야 할 필요가 있습니다.

 

  PHP에서의 예제 소스코드는 다음과 같습니다. Google API를 위한 SDK가 설치되어 있어야 합니다.

 

$client = new Google_Client();
$client->setAuthConfig('/home/ubuntu/IAP.json'); // 구글 개발자 인증 설정
$client->addScope('https://www.googleapis.com/auth/androidpublisher');
$service = new Google_Service_AndroidPublisher($client);
$package_name = "{어플리케이션 패키지 이름}";
$product_id = $this->input_check('product_id');
$purchase_token = $this->input_check('purchase_token');
if($product_id == "" || $purchase_token == "") {
	// 상품 ID와 구매 토큰은 반드시 전달 받아야 합니다.
	exit;
}
try {
	$list = $service->inappproducts->listInappproducts($package_name);
	$amount = -1;
	$purchased_price = -1;
	foreach ($list as $key => $value) {
		// 상품 ID를 기준으로 검색하여 상품을 얼마에 구매했는지 확인
		if($list[$key]['sku'] == $product_id) {
			$str = $list[$key]['listings']['en-US']['title'];
			$purchased_price = $list[$key]['defaultPrice']['priceMicros'];
			$amount = preg_replace("/[^0-9]*/s", "", $str);
			break;
		}
	}
	if($amount == null || $purchased_price == null) {
		// "구글 개발자 콘솔 오류입니다. 관리자에게 문의하세요."
		exit;
	}
	// 구매 가격은 Micro가 기준
	$purchased_price /= 1000000;
	// 구글 API 레퍼런스: https://developers.google.com/android-publisher/api-ref/purchases/products
	$purchase = $service->purchases_products->get($package_name, $product_id, $purchase_token);
	// 해당 주문의 구매 상태와 구매 타입을 확인
	$purchase_state = $purchase->getPurchaseState();
	$purchase_type = $purchase->getPurchaseType();
	$order_id = $purchase->getOrderId();
	if($purchase_state == '1') {
		// 결제 취소 된 주문 내역입니다.
		exit;
	}
	// 이전에 처리 된 토큰인지 확인
	$data['order_no'] = $order_id;
	result= $this->model_member->check_buy($data);	// 포인트 구매 내역 DB 쿼리를 요청합니다.
	if($result != 0) {
		$this->output->set_status_header(403); // 이미 처리 된 주문 내역입니다.
		exit;
	}
	// 이전에 처리 된 적 없는 토큰인 경우 결제 완료
	if($purchase_type == '0') { // 테스트 결제일 때는 purchase_type 값이 0임.
		// 테스트 결제 완료
	}
	else if($purchase_type == null) { // 진짜 결제일 때는 purchase_type 값이 전달되지 않음.
		// 진짜 결제 완료
	}
} catch (Exception $e) {
	$this->output->set_status_header(505); // 상품 ID 혹은 구매 토큰 값이 바르지 않습니다.
	exit;
}

 

  유의할 점은 실제 배포 때와 테스트 때의 구매 타입(Purchase Type)의 값이 다르다는 것입니다. 테스트(Test) 환경에서 결제를 할 때는 purchaseType의 값이 0으로 날아오지만, 실제 배포 환경에서는 purchaseType의 값이 null입니다. 따라서 일반적인 일회성 결제인 경우 purchaseType이 0인지, null인지에 따라서 다르게 처리해주시면 됩니다.

 

 

728x90
반응형