PNS(Payment Notification Service)활용

인앱상품 결제 또는 결제취소 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

msgVersion

Y

메시지 버전

  • 개발(Sandbox) : 2.0.0D

  • 상용(상용테스트) : 2.0.0

packageName : String

Y

앱의 패키지 네임

productId : String

Y

인앱상품의 상품 ID

messageType : String

Y

"SINGLE_PAYMENT_TRANSACTION" 고정

purchaseId : String

Y

구매 ID

developerPayload : String

N

구매건을 식별하기 위해 개발사에서 관리하는 식별자

purchaseTimeMillis : Long

Y

원스토어 결제 시스템에서 결제가 완료된 시간(timestamp:Long)

purcahseState : enum

Y

  • COMPLETED : 결제완료

  • CANCELED : 취소

price : Number

Y

결제 금액

productName : String

N

구매요청 시 개발사가 customized 인앱상품 제목을 설정한 경우 전달

paymentTypeList : object[]

Y

결제 정보

paymentMethod : String

Y

결제 수단 (상세 내용은 아래 paymentMethod 정의 참고)

amount : Number

Y

결제 금액

billingKey

N

확장 기능용 결제 키

isTestMdn

N

시험폰 여부(true : 시험폰, false : 시험폰 아님)

signature : String

Y

본 메시지에 대한 signature

PNS(Payment Notification Service) message 예시

{
  "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결제수단 명칭설명

DCB

휴대폰결제

통신사 요금청구서에 '정보이용료' 항목으로 청구

PHONEBILL

휴대폰 소액결제

통신사 요금청구서에 '소액결제' 항목으로 청구

ONEPAY

ONE pay

원스토어가 제공하는 신용카드 간편결제

ONEPAYBANKACCT

ONE pay 계좌결제

원스토어가 제공하는 계좌 간편결제

ONEPAYDCB

ONE pay 휴대폰결제

원스토어가 제공하는 간편 휴대폰결제

ONEPAYPHONEBILL

ONE pay 휴대폰 소액결제

원스토어가 제공하는 간편 소액결제

CREDITCARD

신용카드

일반 신용카드 결제

11PAY

11Pay

SK플래닛이 제공하는 신용카드 간편결제

NAVERPAY

N pay

네이버에서 제공하는 네이버페이 결제

CULTURELAND

컬쳐캐쉬

한국문화진흥에서 제공한는 컬쳐캐쉬 결제

TMEMBERSHIP

T멤버십

SK텔레콤이 제공하는 T멤버십 결제

OCB

OK cashbag

SK플래닛이 제공하는 OK캐쉬백 결제

GAMECASH

게임캐쉬

원스토어 게임캐쉬 결제

ONESTORECASH

원스토어 캐쉬

원스토어 캐쉬 결제

ONESTORECOUPON

원스토어 쿠폰

원스토어 쿠폰 결제

TMONEY

모바일 T Money

티모넷이 제공하는 모바일티머니 결제

KTMEMBERSHIP

KT멤버쉽

KT 멤버쉽 결제

LGMEMBERSHIP

U+멤버쉽

LG U+ 멤버쉽 결제

Signature 검증 방법

아래 코드를 사용하면 signature에 대한 위변조 여부를 확인할 수 있습니다.

  • 코드 내 PublicKey는 '개발자센터 > Apps > In-App정보 > 인증 및 라이선스'에서 제공되는 라이선스 키를 의미합니다. 라이선스 키에 대한 상세한 내용은 인앱결제 적용을 위한 사전준비 페이지 내 '라이선스 키 및 OAuth 인증 정보 확인' 부분을 참고하시기 바랍니다.

JAVA

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

<?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

# -*- 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

msgVersion

Y

메시지 버전

  • 개발(Sandbox) : 2.0.0D

  • 상용(상용테스트) : 2.0.0

packageName : String

Y

앱의 패키지 네임

productId : String

Y

인앱상품의 상품 ID

messageType : String

Y

"SINGLE_PAYMENT_TRANSACTION" 고정

purchaseId : String

Y

구매 ID

developerPayload : String

N

구매건을 식별하기 위해 개발사에서 관리하는 식별자

purchaseTimeMillis : Long

Y

원스토어 결제 시스템에서 결제가 완료된 시간(timestamp:Long)

purcahseState : enum

Y

  • COMPLETED : 결제완료

  • CANCELED : 취소

price : Number

Y

결제 금액

productName : String

N

구매요청 시 개발사가 customized 인앱상품 제목을 설정한 경우 전달

paymentTypeList : object[]

Y

결제 정보

paymentMethod : String

Y

결제 수단 (상세 내용은 아래 paymentMethod 정의 참고)

amount : Number

Y

결제 금액

billingKey

N

확장 기능용 결제 키

isTestMdn

N

시험폰 여부(true : 시험폰, false : 시험폰 아님)

signature : String

Y

본 메시지에 대한 signature

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결제수단 명칭설명

DCB

휴대폰결제

통신사 요금청구서에 '정보이용료' 항목으로 청구

PHONEBILL

휴대폰 소액결제

통신사 요금청구서에 '소액결제' 항목으로 청구

ONEPAY

ONE pay

원스토어가 제공하는 신용카드 간편결제

ONEPAYBANKACCT

ONE pay 계좌결제

원스토어가 제공하는 계좌 간편결제

ONEPAYDCB

ONE pay 휴대폰결제

원스토어가 제공하는 간편 휴대폰결제

ONEPAYPHONEBILL

ONE pay 휴대폰 소액결제

원스토어가 제공하는 간편 소액결제

CREDITCARD

신용카드

일반 신용카드 결제

11PAY

11Pay

SK플래닛이 제공하는 신용카드 간편결제

NAVERPAY

N pay

네이버에서 제공하는 네이버페이 결제

CULTURELAND

컬쳐캐쉬

한국문화진흥에서 제공한는 컬쳐캐쉬 결제

TMEMBERSHIP

T멤버십

SK텔레콤이 제공하는 T멤버십 결제

OCB

OK cashbag

SK플래닛이 제공하는 OK캐쉬백 결제

GAMECASH

게임캐쉬

원스토어 게임캐쉬 결제

ONESTORECASH

원스토어 캐쉬

원스토어 캐쉬 결제

ONESTORECOUPON

원스토어 쿠폰

원스토어 쿠폰 결제

TMONEY

모바일 T Money

티모넷이 제공하는 모바일티머니 결제

KTMEMBERSHIP

KT멤버쉽

KT 멤버쉽 결제

LGMEMBERSHIP

U+멤버쉽

LG U+ 멤버쉽 결제

Signature 검증 방법

아래 코드를 사용하면 signature에 대한 위변조 여부를 확인할 수 있습니다.

  • 코드 내 PublicKey는 '개발자센터 > Apps > In-App정보 > 인증 및 라이선스'에서 제공되는 라이선스 키를 의미합니다. 라이선스 키에 대한 상세한 내용은 인앱결제 적용을 위한 사전준비 페이지 내 '라이선스 키 및 OAuth 인증 정보 확인' 부분을 참고하시기 바랍니다.

JAVA

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

<?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

# -*- 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