인앱상품 결제 또는 결제취소 transaction이 발생하면 원스토어는 개발사 서버로 notification을 전송합니다.
주의사항
Notification은 발송/수신 서버의 상태에 따라 지연 또는 유실될 수 있으므로, notification 수신을 기준으로 상품(서비스)을 제공해서는 안됩니다.
상품은 구매완료 응답(또는 소비 처리완료)을 기준으로 제공되어야 하며, notification은 결제 확인, 취소 시 아이템 회수 등의 목적으로 활용하는 것을 권장합니다.
정상적인 결제 건인지 서버-to-서버로 확인하기를 원하신다면 PNS notification을 이용하는 대신, 관련 서버 API (getPurchaseDetail 또는 getPurchaseDetailByProductId)로 조회하는 것을 권장합니다.
원스토어는 검증 및 모니터링 목적으로 결제 테스트를 진행할 수 있으며, 해당 테스트 건들도 결제/결제취소 시 동일하게 notification이 발송됩니다.원스토어가 진행한 결제 테스트 내역은 주기적으로 원스토어에서 자체 취소 처리합니다.
PNS(Payment Notification Service) 소개
인앱상품 결제 또는 결제취소 transaction이 발생하면 원스토어는 개발사 서버로 notification을 전송합니다.
주의사항
Notification은 발송/수신 서버의 상태에 따라 지연 또는 유실될 수 있으므로, notification 수신을 기준으로 상품(서비스)을 제공해서는 안됩니다.
상품은 구매완료 응답(또는 소비 처리완료)을 기준으로 제공되어야 하며, notification은 결제 확인, 취소 시 아이템 회수 등의 목적으로 활용하는 것을 권장합니다.
정상적인 결제 건인지 서버-to-서버로 확인하기를 원하신다면 PNS notification을 이용하는 대신, 관련 서버 API (getPurchaseDetail 또는 getPurchaseDetailByProductId)로 조회하는 것을 권장합니다.
원스토어는 검증 및 모니터링 목적으로 결제 테스트를 진행할 수 있으며, 해당 테스트 건들도 결제/결제취소 시 동일하게 notification이 발송됩니다.원스토어가 진행한 결제 테스트 내역은 주기적으로 원스토어에서 자체 취소 처리합니다.
PNS(Payment Notification Service) 소개
PNS 수신 서버 URL 설정
PNS를 수신 받을 개발사 서버의 URL은 '개발자센터 > Apps > 상품 선택 > In-App정보' 메뉴에서 'Payment Notification' 버튼을 클릭하면 설정할 수 있습니다.
URL은 Sandbox(개발용) 결제환경 및 상용(상용테스트 포함) 결제환경을 각각 설정할 수 있으며, 개발용/상용 서버가 동일할 경우 동일한 URL을 입력하시면 됩니다.
PNS(Payment Notification Service) message format (원스토어 > 개발사 서버)
Parameter Name:Type 필수여부 Description "SINGLE_PAYMENT_TRANSACTION" 고정
developerPayload : String
구매건을 식별하기 위해 개발사에서 관리하는 식별자
purchaseTimeMillis : Long
원스토어 결제 시스템에서 결제가 완료된 시간(timestamp:Long)
구매요청 시 개발사가 customized 인앱상품 제목을 설정한 경우 전달
paymentTypeList : object[]
결제 수단 (상세 내용은 아래 paymentMethod 정의 참고)
시험폰 여부(true : 시험폰, false : 시험폰 아님)
PNS(Payment Notification Service) message 예시
Copy {
"msgVersion": "2.0.0.D",
"packageName":"com.onestore.pns",
"productId":"0900001234",
"messageType":"SINGLE_PAYMENT_TRANSACTION",
"purchaseId":"SANDBOX3000000004564",
"developerPayload":"OS_000211234",
"purchaseTimeMillis":24431212233,
"purchaseState":"COMPLETED",
"price":10000,
"productName":"GOLD100(+20)"
"paymentTypeList":[
{
"paymentMethod":"DCB",
"amount":3000
},
{
"paymentMethod":"ONESTORECASH",
"amount":7000
}
],
"billingKey" : "36FED4C6E4AC9E29ADAF356057DB98B5CB92126B1D52E8757701E3A261AF49CCFBFC49F5FEF6E277A7A10E9076B523D839E9D84CE9225498155C5065529E22F5",
"isTestMdn" : true,
"signature": "BwJdUVT/iFFT1MKIFZTdkD/y5+b8h4hCuVB3zVrYcT7pMf1wuWrXNZK9ZA1FhUlWPa7C10Do4CDr8k28QOejGOCgiit5RYzL1tF5eRjFkY66oD3qfNvexkt5wwVjJP5EYyzqwCDVkbx004eGUX46LzaxVV7i137e4KyUrdk9Q5c="
}
paymentMethod(원스토어 결제수단) 정의
paymentMethod 결제수단 명칭 설명 통신사 요금청구서에 '정보이용료' 항목으로 청구
통신사 요금청구서에 '소액결제' 항목으로 청구
Signature 검증 방법
아래 코드를 사용하면 signature에 대한 위변조 여부를 확인할 수 있습니다.
코드 내 PublicKey는 '개발자센터 > Apps > In-App정보 > 인증 및 라이선스'에서 제공되는 라이선스 키를 의미합니다.
라이선스 키에 대한 상세한 내용은 인앱결제 적용을 위한 사전준비 페이지 내 '라이선스 키 및 OAuth 인증 정보 확인' 부분을 참고하시기 바랍니다.
JAVA
Copy import java . security . PublicKey ;
import org . apache . commons . codec . binary . Base64 ;
import org . codehaus . jackson . JsonNode ;
import org . codehaus . jackson . map . ObjectMapper ;
import org . codehaus . jackson . node . ObjectNode ;
public class SignatureVerifier {
private static final String SIGN_ALGORITHM = "SHA512withRSA" ;
private ObjectMapper mapper = new ObjectMapper() ;
boolean verify ( String rawMsg , PublicKey key) throws Exception {
// JSON 메시지에서 signature를 추출한다.
JsonNode root = mapper . readTree (rawMsg);
String signature = root . get ( "signature" ) . getValueAsText ();
((ObjectNode)root) . remove ( "signature" );
// 추출한 signature가 올바른 값인지 검증한다.
Signature sign = Signature . getInstance (SIGN_ALGORITHM);
sign . initVerify (key);
sign . update ( root . toString () . getBytes ( "UTF-8" ));
return sign . verify ( Base64 . decodeBase64 (signature));
}
}
PHP
Copy <? php
function formatPublicKey ($publicKey) {
$BEGIN = "-----BEGIN PUBLIC KEY-----" ;
$END = "-----END PUBLIC KEY-----" ;
$pem = $BEGIN . "\n" ;
$pem .= chunk_split ( $publicKey , 64 , "\n" ) ;
$pem .= $END . "\n" ;
return $pem;
}
function formatSignature ($signature) {
return base64_decode ( chunk_split ( $signature , 64 , "\n" )) ;
}
// Sample message
$sampleMessage = '{"msgVersion":"2.0.0.D","purchaseId":"SANDBOX3000000004564","developerPayload":"OS_000211234","packageName":"com.onestore.pns","productId":"0900001234","messageType":"SINGLE_PAYMENT_TRANSACTION","purchaseMillis":24431212233,"purchaseState":"COMPLETED","price":20000,"productName":"한글은?GOLD100(+20)","paymentTypeList":[{"paymentMethod":"DCB","amount":3000},{"paymentMethod":"ONESTORECASH","amount":7000}],"billingKey":"36FED4C6E4AC9E29ADAF356057DB98B5CB92126B1D52E8757701E3A261AF49CCFBFC49F5FEF6E277A7A10E9076B523D839E9D84CE9225498155C5065529E22F5","isTestMdn":true,"signature":"MNxIl32ws+yYWpUr7om+jail4UQxBUXdNX5yw5PJKlqW2lurfvhiqF0p4XWa+fmyV6+Ot63w763Gnx2+7Zp2Wgl73TWru5kksBjqVJ3XqyjUHDDaF80aq0KvoQdLAHfKze34cJXKR/Qu8dPHK65PDH/Vu6MvPVRB8TvCJpkQrqg="}';
// Sample public key
$publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMzpWJoK1GSOrr4juma5+sREYjdCW8/xSd9+6z6PAkUH5af97wy8ecfkLtP9LK5VskryfDlcOjfu0BgmHYntAqKT7B4KWk8jWbJ8VHUpp30H95UbcnCRFDqpEtwYzNA5gNMYKtAdbL41K8Fbum0Xqxo65pPEI4UC3MAG96O7X1WQIDAQAB";
// Parse JSON message
$jsonArr = json_decode ( $sampleMessage , true ) ;
// Extract and remove signature
$signature = $jsonArr[ "signature" ];
unset ( $jsonArr[ "signature" ] ) ;
$originalMessage = json_encode ( $jsonArr , JSON_UNESCAPED_UNICODE ) ;
// Veify
$formattedKey = formatPublicKey ( $publicKey ) ;
$formattedSign = formatSignature ( $signature ) ;
$hash_algorithm = 'sha512' ;
$success = openssl_verify ( $originalMessage , $formattedSign , $formattedKey , $hash_algorithm ) ;
if ($success == 1 ) {
echo "verified" ;
}
else {
echo "unverified" ;
}
?>
Python
Copy # -*- coding: utf-8 -*-
import json
from base64 import b64decode
from collections import OrderedDict
from Crypto . Hash import SHA512
from Crypto . PublicKey import RSA
from Crypto . Signature import PKCS1_v1_5
import sys
reload (sys)
sys . setdefaultencoding ( 'utf-8' )
hash = "SHA-512"
def verify ( message , signature , pub_key ):
signer = PKCS1_v1_5 . new (pub_key)
digest = SHA512 . new ()
digest . update (message)
return signer . verify (digest, signature)
jsonData = json . loads (rawMsg, encoding = 'utf-8' , object_pairs_hook = OrderedDict)
signature = jsonData [ 'signature' ]
del jsonData [ 'signature' ]
originalMessage = json . dumps (jsonData, ensure_ascii = False , encoding = 'utf-8' , separators = ( ',' , ':' ))
RSA . importKey (publickey). publickey ()
print ( verify (originalMessage, b64decode (signature), RSA. importKey (publickey). publickey ()))
PNS 수신 서버 URL 설정
PNS를 수신 받을 개발사 서버의 URL은 '개발자센터 > Apps > 상품 선택 > In-App정보' 메뉴에서 'Payment Notification' 버튼을 클릭하면 설정할 수 있습니다.
URL은 Sandbox(개발용) 결제환경 및 상용(상용테스트 포함) 결제환경을 각각 설정할 수 있으며, 개발용/상용 서버가 동일할 경우 동일한 URL을 입력하시면 됩니다.
PNS(Payment Notification Service) message format (원스토어 > 개발사 서버)
Parameter Name:Type 필수여부 Description "SINGLE_PAYMENT_TRANSACTION" 고정
developerPayload : String
구매건을 식별하기 위해 개발사에서 관리하는 식별자
purchaseTimeMillis : Long
원스토어 결제 시스템에서 결제가 완료된 시간(timestamp:Long)
구매요청 시 개발사가 customized 인앱상품 제목을 설정한 경우 전달
paymentTypeList : object[]
결제 수단 (상세 내용은 아래 paymentMethod 정의 참고)
시험폰 여부(true : 시험폰, false : 시험폰 아님)
PNS(Payment Notification Service) message 예시
12345678910111213141516171819202122232425
{ "msgVersion": "2.0.0.D", "packageName":"com.onestore.pns", "productId":"0900001234", "messageType":"SINGLE_PAYMENT_TRANSACTION", "purchaseId":"SANDBOX3000000004564", "developerPayload":"OS_000211234", "purchaseTimeMillis":24431212233, "purchaseState":"COMPLETED", "price":10000, "productName":"GOLD100(+20)" "paymentTypeList":[ { "paymentMethod":"DCB", "amount":3000 }, { "paymentMethod":"ONESTORECASH", "amount":7000 } ], "billingKey"
: "36FED4C6E4AC9E29ADAF356057DB98B5CB92126B1D52E8757701E3A261AF49CCFBFC49F5FEF6E277A7A10E9076B523D839E9D84CE9225498155C5065529E22F5", "isTestMdn"
: true, "signature": "BwJdUVT/iFFT1MKIFZTdkD/y5+b8h4hCuVB3zVrYcT7pMf1wuWrXNZK9ZA1FhUlWPa7C10Do4CDr8k28QOejGOCgiit5RYzL1tF5eRjFkY66oD3qfNvexkt5wwVjJP5EYyzqwCDVkbx004eGUX46LzaxVV7i137e4KyUrdk9Q5c="}
paymentMethod(원스토어 결제수단) 정의
paymentMethod 결제수단 명칭 설명 통신사 요금청구서에 '정보이용료' 항목으로 청구
통신사 요금청구서에 '소액결제' 항목으로 청구
Signature 검증 방법
아래 코드를 사용하면 signature에 대한 위변조 여부를 확인할 수 있습니다.
코드 내 PublicKey는 '개발자센터 > Apps > In-App정보 > 인증 및 라이선스'에서 제공되는 라이선스 키를 의미합니다.
라이선스 키에 대한 상세한 내용은 인앱결제 적용을 위한 사전준비 페이지 내 '라이선스 키 및 OAuth 인증 정보 확인' 부분을 참고하시기 바랍니다.
JAVA
Copy import java . security . PublicKey ;
import org . apache . commons . codec . binary . Base64 ;
import org . codehaus . jackson . JsonNode ;
import org . codehaus . jackson . map . ObjectMapper ;
import org . codehaus . jackson . node . ObjectNode ;
public class SignatureVerifier {
private static final String SIGN_ALGORITHM = "SHA512withRSA" ;
private ObjectMapper mapper = new ObjectMapper() ;
boolean verify ( String rawMsg , PublicKey key) throws Exception {
// JSON 메시지에서 signature를 추출한다.
JsonNode root = mapper . readTree (rawMsg);
String signature = root . get ( "signature" ) . getValueAsText ();
((ObjectNode)root) . remove ( "signature" );
// 추출한 signature가 올바른 값인지 검증한다.
Signature sign = Signature . getInstance (SIGN_ALGORITHM);
sign . initVerify (key);
sign . update ( root . toString () . getBytes ( "UTF-8" ));
return sign . verify ( Base64 . decodeBase64 (signature));
}
}
PHP
Copy <? php
function formatPublicKey ($publicKey) {
$BEGIN = "-----BEGIN PUBLIC KEY-----" ;
$END = "-----END PUBLIC KEY-----" ;
$pem = $BEGIN . "\n" ;
$pem .= chunk_split ( $publicKey , 64 , "\n" ) ;
$pem .= $END . "\n" ;
return $pem;
}
function formatSignature ($signature) {
return base64_decode ( chunk_split ( $signature , 64 , "\n" )) ;
}
// Sample message
$sampleMessage = '{"msgVersion":"2.0.0.D","purchaseId":"SANDBOX3000000004564","developerPayload":"OS_000211234","packageName":"com.onestore.pns","productId":"0900001234","messageType":"SINGLE_PAYMENT_TRANSACTION","purchaseMillis":24431212233,"purchaseState":"COMPLETED","price":20000,"productName":"한글은?GOLD100(+20)","paymentTypeList":[{"paymentMethod":"DCB","amount":3000},{"paymentMethod":"ONESTORECASH","amount":7000}],"billingKey":"36FED4C6E4AC9E29ADAF356057DB98B5CB92126B1D52E8757701E3A261AF49CCFBFC49F5FEF6E277A7A10E9076B523D839E9D84CE9225498155C5065529E22F5","isTestMdn":true,"signature":"MNxIl32ws+yYWpUr7om+jail4UQxBUXdNX5yw5PJKlqW2lurfvhiqF0p4XWa+fmyV6+Ot63w763Gnx2+7Zp2Wgl73TWru5kksBjqVJ3XqyjUHDDaF80aq0KvoQdLAHfKze34cJXKR/Qu8dPHK65PDH/Vu6MvPVRB8TvCJpkQrqg="}';
// Sample public key
$publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMzpWJoK1GSOrr4juma5+sREYjdCW8/xSd9+6z6PAkUH5af97wy8ecfkLtP9LK5VskryfDlcOjfu0BgmHYntAqKT7B4KWk8jWbJ8VHUpp30H95UbcnCRFDqpEtwYzNA5gNMYKtAdbL41K8Fbum0Xqxo65pPEI4UC3MAG96O7X1WQIDAQAB";
// Parse JSON message
$jsonArr = json_decode ( $sampleMessage , true ) ;
// Extract and remove signature
$signature = $jsonArr[ "signature" ];
unset ( $jsonArr[ "signature" ] ) ;
$originalMessage = json_encode ( $jsonArr , JSON_UNESCAPED_UNICODE ) ;
// Veify
$formattedKey = formatPublicKey ( $publicKey ) ;
$formattedSign = formatSignature ( $signature ) ;
$hash_algorithm = 'sha512' ;
$success = openssl_verify ( $originalMessage , $formattedSign , $formattedKey , $hash_algorithm ) ;
if ($success == 1 ) {
echo "verified" ;
}
else {
echo "unverified" ;
}
?>
Python
Copy # -*- coding: utf-8 -*-
import json
from base64 import b64decode
from collections import OrderedDict
from Crypto . Hash import SHA512
from Crypto . PublicKey import RSA
from Crypto . Signature import PKCS1_v1_5
import sys
reload (sys)
sys . setdefaultencoding ( 'utf-8' )
hash = "SHA-512"
def verify ( message , signature , pub_key ):
signer = PKCS1_v1_5 . new (pub_key)
digest = SHA512 . new ()
digest . update (message)
return signer . verify (digest, signature)
jsonData = json . loads (rawMsg, encoding = 'utf-8' , object_pairs_hook = OrderedDict)
signature = jsonData [ 'signature' ]
del jsonData [ 'signature' ]
originalMessage = json . dumps (jsonData, ensure_ascii = False , encoding = 'utf-8' , separators = ( ',' , ':' ))
RSA . importKey (publickey). publickey ()
print ( verify (originalMessage, b64decode (signature), RSA. importKey (publickey). publickey ()))
Last updated 9 months ago