보상광고 세팅

훌륭한 UX를 유지하면서 수익을 극대화하기위해 벙글은 유저 선택(opt-in) 광고를 추천합니다. 앱에서 자연스러운 전환이 발생하는 시점에 유저 선택 광고를 non-skippable(스킵불가)로 보여주는 것은 높은 수익을 가져옵니다. 유저들은 이 시점에서 비디오 광고를 보고 가상화폐나 프리미엄 컨텐츠 또는 앱 내 물건같은 가치있는 것을 보는데 동의했으므로 훌륭한 UX가 됩니다. 비디오 광고를 끝까지 본 유저에게 주는 보상의 종류나 양은 전적으로 당신에게 달려있습니다. 이를 수행하기 위해 두가지 방법이 있습니다 :

1. 앱 내 보상 (추천)

개요

이것은 server-to-server 콜백 사용의 대안입니다. 유저가 광고를 끝까지 보면, 당신은 이 유저에게 해당 앱 내에서 바로 보상을 줄 수 있습니다.

이것의 주 장점은 적용이 매우 간단하다는 것입니다. 만약 당신이 리플레이 공격에 대해 걱정하지 않고 빠르고 쉬운 방법을 찾고 있다면, 이 방식을 사용하면 됩니다.

적용- iOS

벙글 SDK 델리게이트를 적용하기를 원한다면, iOS SDK 고급 설정 에서 VungleSDKDelegate 메소드 에서 확인할 수 있습니다.

(void)vungleSDKwillCloseAdWithViewInfo: 콜백은 viewInfo dictionary 를 포함합니다.

completedView 키의 값이 YES일때, 유저에게 비디오 시청에 대한 보상을 주면 됩니다.

적용- Android

EventListener interface를 적용하기를 원한다면, Android SDK 고급 설정 에서 확인할 수 있습니다.

onVideoView의 isCompletedView가 true일때, 유저에게 비디오 시청에 대한 보상을 주면 됩니다.

 

2.Server-to-Server 콜백

개요

Server-to-server 콜백은 당신이 광고를 본 유저들에게 게임 내 화폐나 다른 보상들을 제공할 수 있게 합니다. 유저가 광고를 끝까지 봤을 때, 당신은 벙글의 서버에서 당신의 서버로 유저의 액션(광고 시청)이 끝났음을 알리도록 설정할 수 있습니다.

이것의 장점은 제어입니다. 이 방식은 당신에게 서버 사이드에서 바로 업데이트하고 바꿀 수 있게 하기 때문에 업데이트를 푸쉬할 필요가 없습니다. 또다른 장점은 리플레이 공격을 막기위한 보안 입니다. (데이터 전송이 악의적이거나 부정하게 반복되거나 지연됩니다).

적용

기본 설정

iOS에서는, 당신이 첫번째로 할 일은 당신의 playAd 옵션을 설정하는 것입니다.

vunglePlayAdOptionKeyIncentivized 를 YES로 설정하십시요. playAd 옵션에 대해 더 알고 싶으시면  여기를 참고하세요.

Android에서는, 당신이 첫번째로 할 일은 당신의 AdConfig 오브젝트를 설정하는 것입니다.

setIncentivized 를 true로 설정하십시요. playAd 옵션에 대해 더 알고 싶으시면 여기를 참고하세요.

유저가 보상광고의 80% 이상을 봤다면, 비디오를 끝까지 본 것으로 간주합니다. 벙글은 당신의 콜백 url 서버로 알려줄 것입니다. 콜백 url은 이와 같습니다:

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

또는

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

대쉬보드에 있는 당신의 앱 설정 페이지에서 콜백 URL을 설정하세요(아래 그림을 참고하세요).

 

대부분의 퍼블리셔들은 %user% (추가로 보안을 위해 %txid% 와 %digest%)만을 사용하지만 아래의 모든 변수들이 사용가능합니다:

변수 설명
%user% 아래의 방식으로 Vungle SDK에 제공되는 유저이름. : 
iOS- playAd()에 전달되는 dictionary 옵션의 VunglePlayAdOptionKeyUser 키
Android- playAd()에 전달되는 AdConfig 오브젝트의 setIncentivizedUserId 함수
%udid% 기기 구별을 위한 특별한 식별자
%ifa%

iOS- 기기 구별을 위한 Apple의 특별한 식별자

Android- Google Advertiser ID를 리턴합니다.

%txid% 광고 시청 완료를 구분하는 특별한 transaction ID
%digest%

벙글로부터 오는 콜백을 입증하기 위한 보안토큰 - 보안 섹션에서 자세한 내용을 확인하세요

Note that %user% is the only variable you need to pass in- the rest will come back from Vungle's servers if you include them in the callback URL.

보상 설정

이제 당신은 광고 시청을 마친 유저에게 보상을 줄 수 있습니다. 이 유저에게 무엇을 줄까요? 만약 당신이 매번 보석들을 준다면 매우 간단할 것입니다. 그러나 당신이 좀 더 진보된 방식을 사용하고 싶다면? 보상 설정에 정해진 방식은 없지만 여기 우리가 추천하는 방법이 있습니다:

예제- 코인 vs 생명:

당신의 앱이 보상광고를 상점과 게임오버 창에서 보여주도록 되어있다면, 당신은 상점에서 코인을 주고 싶고 게임오버 창에서는 생명을 주고 싶을 것입니다. 각 playAd() 상황에서 당신은 user를 다음과 같이 설정할 수 있습니다:

userName123:coins 또는 userName123:lives

당신의 서버가 벙글의 콜백을 받을 때, 정확한 보상을 위해 %user%를 파싱하면 됩니다!

보안

권한 콜백

벙글로부터 받은 콜백을 확인하기위해, 당신의 앱 설정에서 Secret Key for Secure Callback를 체크가세요. 이는 다음과 같은 시크릿키를 생성할 것입니다:

4YjaiIualvm8/4wkMBRH8pctlqB1NyzhK3qUGUar+Zc=

 

당신은 이 키를 아래의 방식으로 콜백의 출처를 확인하기위해 이용할 수 있습니다:

  1. 콜론으로 구분되는 transaction ID와 시크릿키를 연결하여 raw transaction verification string 을 만듭니다: transactionString = secretKey + ":" + %txid%
  2. SHA-256 알고리즘을 두 번 적용해 transactionString의 바이트를 해싱합니다.
  3. 2회 연속 SHA-256 해싱 결과를 hex-encoding하여 transaction 인증 토큰을 생성합니다. 다음과 같이 보일 것입니다 :transactionToken = 870a0d13da1f1670b5ba35f27604018aeb804d5e6ac5c48194b2358e6748e2a8
  4. 당신이 생성한 transactionToken이 %digest% 로 전달된 콜백의 querystring과 같은지 확인합니다.

리플레이 공격 막기

당신의 서버에 수차례 반복되는 콜백들을 막기위해, 인증된 transaction ID를 저장하고 복사된 transaction ID을 포함하는 이후의 콜백들을 거부하십시요. Transaction 아이디는 timestamp를 포함하기 때문에, 저장해야 하는 transaction ID들의 수를 제한 할 수 있고 아래와 같이 적시의 콜백에 대한 차단을 이용하여 중복을 체크 할 수 있습니다:

  1. transaction ID로부터 timestamp (1000분의 1초)를 추출합니다: transactionMillis = transactionId.substringAfter(":")
  2. transactionMillis 가 콜백 차단 시간 이후인지 확인하고, transactionId 가 콜백 차단 이후에 발생하지 않았는지 확인해야 합니다.

샘플 코드

아래의 샘플코드들은 당신이 server-to-server 콜백 보안을 수행하는데 도움을 줄 것입니다! 다음과 같은 예제들이 있습니다: Node.js, Java, Python, and 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) {
  // Does the transaction have a reasonable format?
  var tx_timestamp = getTransactionTimestamp(transaction_id);
  if (tx_timestamp === null) { return false; }

  // Is the transaction within a reasonable time range?
  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);
}

// Have we seen this transaction before?
// NOTE: To scale beyond just this one node process, you'll need to put this in some
// sort of centralized datastore. Any one that supports atomic insertions into a set
// should do. The Redis database, with its SADD command, would be a good place to start.
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) {
      // invalid transaction ID format
    }
    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 if transactionId is new, store the transactionId with its associated transactionMillis
    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);
      // invalid timestamp
    }
  }

  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
또 다른 질문이 있으십니까? 문의 등록

댓글