인앱 구매 인증 #

수익 창출 모델 및 도구의 범위는 광고 지원, 소액 결제, 프리미엄, 일회성 구매 및 이들 사이에서 매우 다양합니다. 이러한 솔루션에서 핵심 도구는 인앱 구매(IAP)입니다. IAP를 통해 잠금 해제, 게임 내 소모품, 프리미엄 액세스 구독 등을 위한 단일 구매가 가능합니다.

가장 일반적인 인앱 구매 구현을 쉽게 공격할 수 있는 경우가 매우 많습니다. 일반적으로 다음과 같은 사항에 중점을 둡니다.

  • 성공을 나타내는 클라이언트 가짜 구매 응답 제공
  • 유효한 구매 응답을 여러 번 재생
  • 구매 응답을 다른 클라이언트와 공유하여 여러 플레이어가 단일 구매를 통해 보상 받기

인앱 구매를 위해서는 신뢰할 수 있는 출처가 필요합니다. Nakama는 구매 및 구매 내역을 확인하고 추적하여 다음과 같은 상당한 취약점 및 문제점을 해결합니다.

IssueDescription
Fake PurchasesNakama는 Apple, Google 및 Huawei 서비스에 직접 연결하여 들어오는 모든 구매 영수증의 유효성을 확인합니다. 이 확인은 완전히 클라이언트 코드 외부에 있으며, 이를 사용해 가로채거나 변조할 수 없습니다. 모든 구매 영수증은 매번 확인되며 유효하지 않은 영수증은 거부됩니다.
Replay Attacks모든 트랜잭션이 기록되어 동일한 구매 토큰이나 영수증을 여러 번 제출하지 못하게 됩니다.
Receipt Sharing성공적인 트랜잭션은 해당 트랜잭션을 제출하는 계정에 바인딩됩니다. 유효한 트랜잭션이라도 서로 다른 사용자가 관련 보상을 받으려고 똑같은 트랜잭션을 제출할 수는 없습니다.
Product Mismatches검증된 각 구매 영수증은 구매와 제품의 연계에 사용할 수 있는 데이터(예: 제품 ID)를 노출하므로, 유효한(저렴한) 구매를 사용해 다른(값비싼) 보상을 받아내려는 공격이 방지됩니다.
Single Source of TruthNakama가 모든 트랜잭션의 내부 기록을 유지하는 동안, 검증을 위해 항상 원격 결제 공급자를 사용합니다.

인앱 구매 인증은 플랫폼에 관계없이 Apple, GoogleHuawei 구매에 사용할 수 있습니다. 단일 제품 및 구독 구매가 모두 지원됩니다.

Unity IAP를 통한 Apple 및 Google 구매도 지원됩니다.

인증 결과 해석 #

인증 결과에는 검증된 구매 목록이 포함됩니다.

Apple은 하나의 영수증에 여러 구매를 포함할 수 있으므로 결과 목록의 인증된 각 구매에는 인증된 구매와, 이전에 확인되었는지 여부를 나타내는 boolean 값만 포함됩니다. 이전에 Nakama에 의해 구매가 이미 인증된 경우 “이전 확인” 값은 개발자가 새로운 구매를 구별하고 재생 공격으로부터 보호할 수 있도록 true이(가) 됩니다.

Google 및 Huawei의 경우, 각 인증은 단일 구매에 해당하며 인증 응답 목록에 포함되며 위와 같이 “이전 확인” 값도 있습니다.

인증된 각 구매에는 개발자가 어떤 이유로든 필요로 하는 경우 공급자 인증 응답의 페이로드도 포함됩니다.

구매/영수증이 유효하지 않거나 어떤 이유로든 인증하지 못하거나 공급자에게 연결할 수 없으면 오류가 반환됩니다.

Apple #

Nakama는 iOS에서 제품 및 구독에 대한 구매 인증을 지원합니다.

Apple 구매 영수증은 인증을 위해 Apple로 전송됩니다. Apple에서 제안한 대로 Production 및 Sandbox 서버는 모두 영수증 인증에 사용됩니다.

설정 #

앱 스토어에 대한 영수증을 인증하기 위해 Nakama는 앱의 공유 암호를 요구합니다.

  1. 앱의 인앱 구매 관리 섹션의 App Store Connect에서 공유 암호 설정:

    Apple App Store Connect
    Apple App Store Connect

  2. Nakama 구성에 사용할 공유 암호를 기록해 두십시오.

    Apple App Store Connect 공유 암호
    Apple App Store Connect 공유 암호

  3. Nakama의 iap.apple.shared_password 구성 플래그 값을 위에서 생성한 공유 암호 값으로 설정합니다.

구매 인증 #

Nakama는 iOS 7 이상 영수증 인증만 지원합니다.

Apple 영수증에는 여러 구매가 포함될 수 있으며 Nakama는 모든 구매를 인증하지만 구매 기록은 개별적으로 저장합니다.

Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/purchase/apple \
  --user 'defaultkey:' \
  --data '{"receipt":"base64_encoded_receipt_data"}'
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local function validate_receipt(receipt)
  local result = client.validate_purchase_apple(receipt)
  if result.error then
    print(result.message)
    return
  end
  pprint(result)
end

-- Use https://defold.com/extension-iap/
iap.set_listener(function(self, transaction, error)
  if not error then
    validate_receipt(transaction.receipt)
  end
end)
iap.buy("com.defold.nakama.goldbars-10")
Client
1
2
3
4
5
6
string appleReceipt = "<receipt>";
var response = await client.ValidatePurchaseAppleAsync(session, appleReceipt);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
Client
1
2
3
4
5
const appleReceipt = "<receipt>";
const result = await client.validatePurchaseApple(session, appleReceipt);
result.validatedPurchases.forEach(validatedPurchase => {
  console.info("Validated purchase:", validatedPurchase)
});
Client
1
2
3
4
5
6
7
var apple_receipt = "<receipt>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_apple_async(session, apple_receipt), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
Client
1
2
3
4
5
6
7
8
POST /v2/iap/purchase/apple
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "receipt": "base64_encoded_Apple_receipt_payload"
}

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.

제공되는 런타임 구매 인증 기능은 기능 참조 페이지를 참조하세요.

구독 인증 #

Client
1
2
3
4
string appleReceipt = "<receipt>";
var response = await client.ValidateSubscriptionAppleAsync(session, appleReceipt);

System.Console.WriteLine("Validated subscription: " + response.ValidatedSubscription);
Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/subscription/apple \
  --user 'defaultkey:' \
  --data '{"receipt":"base64_encoded_receipt_data"}'
Client
1
2
3
4
5
6
7
8
POST /v2/iap/subscription/apple
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "receipt": "base64_encoded_Apple_receipt_payload"
}

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language gdscript has not been found. Please choose another language to show equivalent examples.
Code snippet for this language JavaScript/Cocos2d-js has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.

제공된 런타임 구독 인증 기능에 대한 기능 참조 페이지를 참조하세요.

Google #

Nakama는 Android에서 제품 및 구독에 대한 구매 인증을 지원합니다.

설정 #

Play 스토어에 대한 영수증을 인증하기 위해 Nakama에서 Google 서비스 계정 ClientEmailPrivateKey을(를) 요청합니다. 이런 값을 구하려면:

  1. Google API Console에서 서비스 계정 설정:

    서비스 계정 생성
    서비스 계정 생성

  2. 서비스 계정을 만들었으면 키를 생성해야 합니다:

    키 생성
    키 생성

  3. 이 키를 JSON 파일로 다운로드합니다:

    JSON 키 생성
    JSON 키 생성

  4. 이 JSON 파일을 열고 ClientEmailPrivateKey 값을 추출합니다.

  5. 해당 값을 iap.google.client_emailiap.google.private_key에 대한 각각의 Nakama 구성 값으로 설정합니다.

  6. 마지막으로 구매 인증 API에 대한 액세스 권한을 Nakama에 부여해야 합니다. Google Play 개발자 콘솔에서 설정 > API 액세스로 이동합니다:

    API 액세스 생성
    API 액세스 생성

  7. 이전 단계에서 만든 서비스 계정이 위에 나열되어야 합니다. 서비스 계정에 대한 액세스 권한을 부여하여 API에 액세스하고 서비스 계정에 가시성, 재무 데이터 보기주문 관리에 대한 액세스 권한을 부여해야 합니다. 이러한 권한은 Nakama가 Google Play에 대한 영수증 인증에 필요합니다.

    액세스 부여
    액세스 부여

  8. 사용자 및 권한으로 이동하여 서비스 계정이 올바르게 설정되었는지 확인합니다.

    액세스 권한이 있는 사용자 나열
    액세스 권한이 있는 사용자 나열

구매 인증 #

예상되는 Google 영수증은 Purchase.getOriginalJson()에서 반환한 문자열입니다. 자세한 내용은 공식 [Google 개발자 문서](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getOriginalJson()를 참조하세요.
Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/purchase/google \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data"}'
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local function validate_receipt(receipt)
  local result = client.validate_purchase_google(receipt)
  if result.error then
    print(result.message)
    return
  end
  pprint(result)
end

-- Use https://defold.com/extension-iap/
iap.set_listener(function(self, transaction, error)
  if not error then
    validate_receipt(transaction.receipt)
  end
end)
iap.buy("com.defold.nakama.goldbars-10")
Client
1
2
3
4
5
6
string googleReceipt = "<receipt>";
var response = await client.ValidatePurchaseGoogleAsync(session, googleReceipt);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
Client
1
2
3
4
5
const googleReceipt = "<receipt>";
const result = await client.validatePurchaseGoogle(session, googleReceipt);
result.validatedPurchases.forEach(validatedPurchase => {
  console.info("Validated purchase:", validatedPurchase)
});
Client
1
2
3
4
5
6
7
var google_receipt = "<receipt>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_google_async(session, google_receipt), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
Client
1
2
3
4
5
6
7
8
POST /v2/iap/purchase/google
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data"
}

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.

제공되는 런타임 구매 인증 기능은 기능 참조 페이지를 참조하세요.

구독 인증 #

Client
1
2
3
4
string googleReceipt = "<receipt>";
var response = await client.ValidateSubscriptionGoogleAsync(session, googleReceipt);

System.Console.WriteLine("Validated subscription: " + response.ValidatedSubscription);
Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/subscription/google \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data"}'
Client
1
2
3
4
5
6
7
8
POST /v2/iap/subscription/google
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data"
}

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language gdscript has not been found. Please choose another language to show equivalent examples.
Code snippet for this language JavaScript/Cocos2d-js has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.

제공된 런타임 구독 인증 기능에 대한 기능 참조 페이지를 참조하세요.

Huawei #

Nakama는 IAP 인증 서비스를 기준으로 Huawei 구매를 인증합니다. Huawei에서 제안한 바와 같이 Huawei 서비스에 연락하기 전에 제공된 서명에 대해 구매 데이터의 유효성도 확인됩니다. 어떤 이유로든 데이터가 유효하지 않은 경우 Huawei의 인증 서비스에서 인증하기 전에 구매는 거부됩니다.

구매 인증 #

Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/purchase/huawei \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data","signature":"purchase_data_signature"}'
Client
1
-- Huawei purchases are not yet supported by https://defold.com/extension-iap/
Client
1
2
3
4
5
6
7
string huaweiReceipt = "<receipt>";
string huaweiSignature = "<signature>";
var response = await client.ValidatePurchaseHuaweiAsync(session, huaweiReceipt, huaweiSignature);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
Client
1
2
3
4
5
6
const huaweiReceipt = "<receipt>";
const huaweiSignature = "<signature>";
const result = await client.validatePurchaseHuawei(session, huaweiReceipt, huaweiSignature);
result.validatedPurchases.forEach(validatedPurchase => {
  console.info("Validated purchase:", validatedPurchase)
});
Client
1
2
3
4
5
6
7
8
var huawei_receipt = "<receipt>"
var huawei_signature = "<signature>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_huawei_async(session, huawei_receipt, huawei_signature), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
Client
1
2
3
4
5
6
7
8
9
POST /v2/iap/purchase/huawei
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data",
  "signature":"purchase_data_signature"
}

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.

제공되는 런타임 구매 인증 기능은 기능 참조 페이지를 참조하세요.

Unity IAP #

Unity IAP는 Apple 및 Google을 비롯한 가장 인기 있는 앱 스토어와의 통합을 지원합니다.

Unity IAP 구매 영수증에는 다음 정보가 포함되어 있습니다.

KeyValue
StoreThe name of the store in use, such as GooglePlay or AppleAppStore.
TransactionIDThis transaction’s unique identifier, provided by the store.
PayloadFor Apple, a Base64 encoded App Receipt. For Google, JSON encoded purchase data and a signature.

구매 인증 #

Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.
Code snippet for this language TypeScript has not been found. Please choose another language to show equivalent examples.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type unityIAP struct {
	  Payload       string `json:"Payload"`
	  Store         string `json:"Store"`
	  TransactionID string `json:"TransactionID"`
}

switch unityIAP.Store {
    case "GooglePlay":
        validatedReceipt, err = nk.PurchaseValidateGoogle(ctx, userID, wrapper.Payload)
    case "AppleAppStore":
        validatedReceipt, err = nk.PurchaseValidateApple(ctx, userID, wrapper.Payload)
    default:
        logger.Warn("Unrecognised store type in Unity IAP.")
      return ErrBadInput
    }

    if err != nil {
        if err == runtime.ErrPurchaseReceiptAlreadySeen {
            logger.WithField("err", err).Warn("Receipt replay attack.")
        } else {
            logger.WithField("err", err).Error("Receipt validation error.")
        }
        return ErrBadInput
}

구독 #

구매 및 구독 인증 외에도 특정 제품 ID로 구독하거나 지정된 사용자의 모든 구독을 나열할 수도 있습니다.

구독하기 #

Client
1
2
3
4
string productId = "<productId>";
var response = await client.GetSubscriptionAsync(session, productId);

System.Console.WriteLine("Subscription: " + response);
Client
1
2
curl "http://127.0.0.1:7350/v2/iap/subscription/{productId} \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
GET /v2/iap/subscription/{productId}
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language gdscript has not been found. Please choose another language to show equivalent examples.
Code snippet for this language JavaScript/Cocos2d-js has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.

제공된 런타임 구독 인증 기능에 대한 기능 참조 페이지를 참조하세요.

사용자 구독 나열 #

Client
1
2
3
4
5
6
7
string userId = "<userId>";
var response = await client.ListSubscriptionsAsync(userId);

foreach (var validatedSubscription in response.ValidatedSubscriptions)
{
    System.Console.WriteLine("Subscription: " + validatedSubscription);
}
Client
1
2
curl "http://127.0.0.1:7350/v2/iap/subscription/limit=<limit>&cursor=<cursor>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
GET /v2/iap/subscription/limit=<limit>&cursor=<cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language gdscript has not been found. Please choose another language to show equivalent examples.
Code snippet for this language JavaScript/Cocos2d-js has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.

제공된 런타임 구독 인증 기능에 대한 기능 참조 페이지를 참조하세요.