인센티브화 광고 설정

목차

왜 사전 동의 삽입 광고를 사용하나요?

Vungle은 우수한 UX를 유지하면서 수익을 극대화하기 위해 사전 동의 삽입 광고를 사용할 것을 권장합니다. 앱의 휴식 시간에 표시됩니다. 삽입 광고를 선택하면 수익을 높일 수 있으며, 특히 건너뛸 수 없게 만든 경우 수익이 극대화됩니다. 또한 이 광고위치에서는 사용자가 비디오 시청을 선택하면 가상 화폐, 프리미엄 콘텐츠 또는 인앱 상품 등 가치 있는 보상을 받기 때문에 뛰어난 UX도 제공합니다. 비디오 시청을 완료한 사용자에게 제공하는 보상 금액과 유형은 전적으로 귀하가 선택할 수 있습니다. 인앱 보상 또는 서버 간 콜백으로 이를 선택할 수 있습니다.

추천 옵션: 인앱 보상

개요

서버 간 콜백 대신 사용할 수 있는 옵션입니다. 사용자가 광고 시청을 완료하거나 다운로드 버튼을 클릭하면 앱에서 바로 사용자에게 보상을 제공할 수 있습니다. 이 접근 방식은 구현이 매우 쉽다는 것이 장점입니다. 이 방식은 재전송 공격을 걱정할 필요가 없는 경우에 사용하는 것이 좋습니다.

Vungle은 이제 동적 템플릿 광고로 다양한 광고 포멧을 제공합니다. 비디오 재생 후에 끝내기 카드가 표시되는 기존 광고 포멧과 달리 Vungle은 비디오 재생 중에 클릭 유도 문안 (CTA) 버튼을 사용할 수 있는 템플릿을 제공합니다. 비디오 광고를 끝까지 시청한 사용자 및 버튼을 클릭한 사용자에게 보상을 제공해야 합니다.

iOS에서 구현

Vungle 시작하기 - iOS SDK v. 5.1 이상의 "델리게이트 콜백" 섹션에서 언급된 VungleSDK 델리게이트를 구현합니다.

- (void)vungleWillCloseAdWithViewInfo:(VungleViewInfo *)info placementID:(NSString *)placementID;

vungleSDKwillCloseAdWithViewInfo 콜백이 completedView 또는 didDownload에 대한 값이 'yes'인 viewInfo 딕셔너리를 전달하는 경우, 사용자가 보상을 받습니다.

Android에서 구현

Vungle 시작하기 - iOS SDK v. 5.1 이상에서 언급된 EventListener 인터페이스를 구현합니다.

public void onAdEnd (String placementReferenceId, boolean wasSuccessfulView, boolean wasCallToActionClicked)

onAdEnd 콜백이 wasSuccessfulView 또는wasCallToActionClicked 중 하나에 관해 'true'를 반환하면 사용자가 보상을 얻습니다.

서버 간 콜백

개요

서버 간 콜백을 사용하면 광고 시청을 완료한 사용자에게 인게임 통화 또는 기타 보상으로 보상을 제공할 수 있습니다. 사용자가 광고 시청을 완료하면 Vungle's servers to your own to notify you of the user' 완료된 액션에서 콜백을 구성할 수 있습니다.

이 접근 방식은 컨트롤이 장점입니다. 이 방법을 사용하면 서버 측에서 직접 변경 및 업데이트가 가능하므로 업데이트를 푸시할 필요가 없습니다. 또한, 재전송 공격(유효한 데이터 전송을 악의적 또는 불법적으로 반복하거나 지연하는 행위)을 방지하는 보안 기능도 제공합니다.

Vungle SDK v. 5.1 이상 구현

SDK v. 5.1부터는 보상 옵션이 대시보드로 이동하여 게시자가 코드를 변경하지 않아도 쉽게 옵션을 변경할 수 있습니다. 대시보드에서 애플리케이션 단계광고위치 단계로 이동한 후, 연필 아이콘 클릭image1.png광고위치 편집으로 이동하여 광고위치 수준에서 보상 옵션을 사용 또는 사용 중지하는 체크박스를 찾습니다.

image2.jpg

Vungle SDK v.1.0 - v.4.1 구현

  • iOS의 경우, playAd 객체에서 vunglePlayAdOptionKeyIncentivized 옵션을 '예'로 설정하고 시작합니다. (이 옵션에 대한 자세한 내용 및 playAd 관련 옵션은 여기에서 확인하십시오.)

  • Android의 경우, AdConfig 객체에서 setIncentivized 옵션을 'true'로 설정하고 시작합니다. (이 옵션에 대한 자세한 내용 및 playAd 관련 옵션은 여기에서 확인하십시오.)

사용자가 인센티브화된 광고를 80% 이상 시청한 경우, 시청을 완료한 것으로 간주합니다. Vungle은 다음과 같은 콜백 URL을 사용하여 서버를 ping합니다.

http://acme.com/bugzBunny/reward?uid=%user%

다음과 같은 URL을 사용할 수도 있습니다.

http://acme.com/bugzBunny/reward? amount=1&uid=%user%&txid=%txid%&digest=%digest%

앱의 대시보드 내 설정에서 콜백 URL을 구성합니다(아래 참조).

CallbackURL.gif

대부분의 퍼블리셔는 보안상의 이유로 %user%, plus%txid%, %digest%만 사용하지만, 다음과 같은 것을 모두 사용할 수 있습니다.

변수

설명

%user%

다음을 통해 Vungle SDK에 제공한 사용자 이름:

●      iOS: playAd()로 전달된 옵션 딕셔너리VunglePlayAdOptionKeyUser

●      Android: playAd()로 전달된 전역 광고 구성 객체setIncentivizedUserId 설정자

%udid%

기종 고유 식별자

%ifa%

●      iOS: Apple의 기종 고유 식별자

●      Android: Google Advertiser ID가 반환됩니다.

%txid%

시청 완료된 건에 대한 고유 트랜잭션 ID

%digest%

콜백이 Vungle에서 왔는지 확인하는 보안 토큰. 자세한 내용은 보안 섹션을 참조하십시오.

변수 중에서 %user%만 전달하면 되고, 나머지는 콜백 URL에 포함하면 Vungle 서버에서 반환됩니다.

보상 구성

이제 광고를 시청한 사용자에게 보상을 제공할 수 있습니다. 보상으로 무엇을 주시겠습니까? 간단한 방법을 원하시면 매번 보석을 줄 수 있습니다. 그보다 발전된 방법을 원하십니까? Vungle에는 내장된 보상 구성 옵션이 없지만, 다음을 추천해드립니다.

예: 코인 vs. 생명

앱 안의 여러 장소에 인센티브화 광고를 유치했다고 가정해보겠습니다. 상점에도 광고가 있고, "게임 오버" 시 세 번에 한 번마다 광고가 게재됩니다. 상점에서는 플레이어에게 코인을 제공하고 게임 오버 때는 생명을 보상해주려고 한다고 하겠습니다. playAd()의 각 인스턴스에 대해 다음과 같이 사용자를 구성합니다.

userName123:coins or userName123:lives 

그런 다음 서버가 Vungle의 콜백을 받으면 %user% 구문을 분석해서 올바른 보상을 합니다.

보안

콜백 인증

Vungle에서 발신한 콜백을 확인하려면 애플리케이션의 보안 콜백 보안 키 확인란을 선택하십시오. 다음과 같은 비밀 키가 생성됩니다.

4YjaiIualvm8/4wkMBRH8pctlqB1NyzhK3qUGUar+Zc=

키를 사용하여 다음과 같이 콜백의 출처를 확인할 수 있습니다.

  1. 다음과 같이 콜론으로 구분된 비밀 키를 트랜잭션 ID와 연결하여 원시 트랜잭션 확인 문자열을 생성합니다.
    transactionString = secretKey + ":" + %txid%
  2. SHA-256 알고리즘을 사용하여 transactionString의 바이트를 두 번 해시합니다.

  3. 순차적인 SHA-256 해싱의 출력 바이트를 16진수로 인코딩하여 트랜잭션 확인 토큰을 생성합니다. 다음과 같이 표시됩니다.
    transactionToken = 870a0d13da1f1670b5ba35f27604018aeb804d5e6ac5c48194b2358e6748e2a8
  4. 생성된 transactionToken이 콜백 쿼리 문자열에서 전송된 것(예: %digest%)과 동일한지 확인합니다.

재전송 공격 방지

서버에 대해 단일 콜백이 여러 번 재전송되는 것을 막으려면 인증된 트랜잭션 ID를 저장하고, 중복 트랜잭션 ID로 향후 콜백을 거부합니다. 트랜잭션 ID에는 타임 스탬프가 포함되어 있으므로 저장해야 하는 트랜잭션 ID의 수를 제한할 수 있고, 다음과 같이 적시 콜백에 대한 차단을 적용하여 중복을 검사할 수 있습니다.

  1. 트랜잭션 ID에서 다음과 같이 타임 스탬프(밀리 초 기준)를 추출합니다.
    transactionMillis = transactionId.substringAfter(":")
  2. transactionMillis가 귀하의 컷오프보다 늦었는지 확인하고, 컷오프 이후 transactionId가 발생하지 않았는지도 확인하십시오.

샘플 코드

이 샘플 코드는 서버 간 콜백 보안 구현에 도움을 줍니다. Node.js, Java, Python 및 Ruby에 대한 예를 보여드립니다.

Node.js

var crypto = require('crypto');

function isTransactionValid(secret, transaction_id, provided_hash) {
  return isTransactionRecent(transaction_id) &&
         isTransactionNew(transaction_id)    &&
         createSecurityDigest(secret, transaction_id) === provided_hash;
}

function getTransactionTimestamp(transaction_id) {
  return parseInt(transaction_id.split(":")[1], 10) || null;
}

function isTransactionRecent(transaction_id) {
  // 트랜잭션의 포멧이 적당한가?
  var tx_timestamp = getTransactionTimestamp(transaction_id);
  if (tx_timestamp === null) { return false; }

  // 트랜잭션이 적당한 시간 범위 안에 있는가?
  var now = new Date().getTime();
  var time_diff = now - tx_timestamp;
  var hour_in_future = -1000 * 60 * 60, three_days_ago = 1000 * 60 * 60 * 24 * 3;
  return (time_diff < three_days_ago && time_diff > hour_in_future);
}

// 이 트랜잭션이 전에 발생한 적이 있는가?
// 참고: 한 개 이상의 노드 프로세스로 확장하려면 이 값을 일종의
// 중앙 집중식 데이터 저장소에 배치해야 합니다. 세트에 아토믹 삽입을 지원하는 경우
// 반드시 해야 합니다. Redis 데이터베이스에서 SADD 명령을 사용해 시작하시면 좋습니다.
var known_transactions = {};
function isTransactionNew(transaction_id) {
  if (known_transactions[transaction_id]) { return false; }

  known_transactions[transaction_id] = true;
  return true;
}

function createSecurityDigest(secret, transaction_id) {
  var firsthash = crypto.createHash("sha256").update(secret + ":" + transaction_id).digest("binary");
  return crypto.createHash("sha256").update(firsthash,"binary").digest("hex");
}

Java

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;

public class ServerCallbackSecurityExample {
  private static final long MAX_CALLBACK_AGE_MILLIS = 7 * 24 * 60 * 60 * 1000;  // 7 days

  /**
   * Checks that a transaction is recent enough, signed with the secretKey, and not a duplicate.
   * 
   * @param transactionId the transaction ID.
   * @param secretKey a shared secret.
   * @param verificationDigest the verification digest sent in the callback querystring.
   */
  public boolean isValidTransaction(String transactionId, String secretKey, String verificationDigest) throws NoSuchAlgorithmException {
    return isRecentTransaction(transactionId)
      && isDigestValid(transactionId, secretKey, verificationDigest)
      && isNewTransaction(transactionId);
  }

  protected boolean isRecentTransaction(String transactionId) {
    boolean isRecent = false;
    try {
      final long minCallbackAgeMillis = System.currentTimeMillis() - MAX_CALLBACK_AGE_MILLIS;
      final long transactionMillis = getTransactionMillis(transactionId);
      isRecent = (transactionMillis > minCallbackAgeMillis);
    }
    catch (ParseException exception) {
      // 잘못된 트랜잭션 ID 포멧
    }
    return isRecent;
  }

  protected boolean isDigestValid(String transactionId, String secretKey, String verificationDigest) throws NoSuchAlgorithmException {
    return createSecurityDigest(transactionId, secretKey)
      .equals(verificationDigest);
  }

  protected boolean isNewTransaction(String transactionId) {
    // TODO transactionId가 new인 경우, 관련된 transactionMillis와 transactionId를 함께 저장합니다.
    return true;
  }

  protected long getTransactionMillis(String transactionId) throws ParseException {
    final int transactionMillisIndex = transactionId.lastIndexOf(":") + 1;
    try {
      if (transactionMillisIndex > 0 && transactionMillisIndex < transactionId.length()) {
        return Long.parseLong(
          transactionId.substring(transactionMillisIndex));
      }
      else {
        throw new ParseException("No timestamp in transaction ID", transactionMillisIndex);
      }
    }
    catch (NumberFormatException exception) {
      throw new ParseException("Invalid transaction ID timestamp", transactionMillisIndex);
      // 잘못된 타임스탬프
    }
  }

  protected String createSecurityDigest(String transactionId, String secretKey) throws NoSuchAlgorithmException {
    final String verificationString = secretKey + ":" + transactionId;
    final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
    return toHexString(
      messageDigest.digest(
        messageDigest.digest(
          verificationString.getBytes(Charset.forName("US-ASCII")))));
  }

  protected String toHexString(byte[] bytes) {
    final StringBuffer hexStringBuffer = new StringBuffer();
    for (final byte byt : bytes) {
      hexStringBuffer.append(
        Integer.toString((byt & 0xff) + 0x100, 16)
          .substring(1));
    }
    return hexStringBuffer.toString();
  }
}

Python

import hashlib, time

def isTransactionValid(secret, transaction_id, input_hash):
  """Returns whether this transaction id / hash should be considered valid"""
  return isTransactionIDRecent(transaction_id) and \
         isTransactionIDNew(transaction_id)    and \
         createSecurityDigest(secret, transaction_id) == input_hash

def getTransactionTimestamp(transaction_id):
  """Will return the unix time (in milliseconds) of this transaction, or None if invalid"""
  parsed = transaction_id.split(":")
  try:
    return int(parsed[1]) if len(parsed) == 2 else None
  except ValueError as e:
    return None

def isTransactionIDRecent(transaction_id):
  """Is this transaction within a reasonable time range?"""
  tx_time = getTransactionTimestamp(transaction_id)

  # Handle bad transaction:
  if tx_time is None:
    return False

  # Handle bad transaction times:
  now = int(time.time() * 1000)
  three_days_ago = now - (1000 * 60 * 60 * 24 * 3)
  one_hour_from_now = now + (1000 * 60 * 60)
  return ( three_days_ago < tx_time < one_hour_from_now )

def isTransactionIDNew(transaction_id, known_transaction_ids=set()):
  """Is this a duplicate transaction?
     NOTE: We only use the Python set for simplicity. For better / more centralized solutions,
     you can use any datastore that supports atomic insertion into a set. For starters, try
     Redis with its "SADD" command."""
  if transaction_id in known_transaction_ids:
    return False

  # Else, valid:
  known_transaction_ids.add(transaction_id)
  return True


def createSecurityDigest(secret, transaction_id):
  """Will return the string that the security hash should have been"""
  firsthash = hashlib.sha256()
  firsthash.update(secret + ":" + transaction_id)

  secondhash = hashlib.sha256()
  secondhash.update(firsthash.digest())

  return secondhash.hexdigest()

Ruby

require "openssl"
require "digest/sha2"
require "base64"
require "time"

# just some helper methods, ignore if using Rails
class Fixnum
  SECONDS_IN_DAY = 24 * 60 * 60
  HOURS_IN_DAY = 24 * 60

  def days
    self * SECONDS_IN_DAY
  end

  def hour
    self * 60 * 60
  end

  def ago
    (Time.now - self)
  end

  def from_now
    (Time.now + self)
  end
end

def transaction_valid?(secret, txid, input_hash)
  transaction_id_recent?(txid) && transaction_id_new?(txid) && create_security_digest(secret, txid) == input_hash
end

def transaction_timestamp(txid)
  arr = txid.split(":")
  return arr[1].to_i if arr.size == 2
end

def transaction_id_recent?(txid)
  tx_time = transaction_timestamp(txid)
  return false if tx_time.nil?

  now = Time.now.to_i
  three_days_ago = 3.days.ago
  one_hour_from_now = 1.hour.from_now
  three_days_ago.to_i < tx_time && tx_time < one_hour_from_now.to_i
end

def transaction_id_new?(txid, transactions = [])
  return false if transactions.include?(txid)
  transactions << txid
  return true
end

def create_security_digest(secret, txid)
  verification_string = "#{secret}:#{txid}"
  first_digest = Digest::SHA2.new(256).update(verification_string)
  Digest::SHA2.new(256).hexdigest(first_digest.digest)
end
또 다른 질문이 있으십니까? 문의 등록

댓글