激励性广告设置

为最大化收入并维持良好的用户体验,Vungle推荐使用选择观看型(观看后获得奖励)间隙广告。奖励性间隙广告会在应用自然暂停时显示,可实现高收入,通常不可跳过。这种广告布置同时也会带来良好的用户体验,因为用户选择观看视频,而且会获得有价值的奖励——比如虚拟货币、高级内容或者应用内物品。用户完成观看视频广告所获奖励的数量和种类完全取决于你。实现这种应用有两种方法:

1. 应用内奖励 (In-App Rewards)

概述

这个方法是服务器到服务器回调的简化版,当用户成功观看完视频广告时,你可以直接在应用中奖励用户。

这样做的主要好处就是实施起来非常简单。如果你在寻找一些快速的实现方法,而且不担心回放攻击的话,那这种方法非常合适。

实施- iOS

你需要实施VungleSDK Delegate委托,该选项可以在 高级设置 的Delegate Methods下面找到。

(void)vungleSDKwillCloseAdWithViewInfo: 这个回调函数会传递一个 viewInfo 字典。

如果参数 completedView 返回值为 YES,请给看完的用户奖励。

实施- Android

你需要实施EventListener接口,该接口设置请查看 高级设定

如果参数 isCompletedView (在 onVideoView 中) 返回值为 true,用户应该获得奖励。

 

2.服务器到服务器回调 (推荐)

概述

服务器到服务器回调可以让你奖励广告观看的用户游戏内货币或其他物品。当用户成功观看完一则广告时,你可以配置一个从Vungle服务器到你自己服务器的回调函数来通知你用户完成观看的动作。

这样做的一个好处就是便于控制。这种方法可让你直接对服务器端的内容做出改变和更新,因此无需再推送更新到app。另一个好处是安全,可防范 重放攻击 (是一种网络攻击,它恶意的欺诈性的重复或拖延正常的数据传输。).

实施

a. 基本设置

当用户观看完一则奖励性广告的80%或更长时,这个动作即被认为是一次完整观看。这时Vungle会根据你的回调URL ping你的服务器,如下:

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

或者

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

在你应用控制板的高级设置(Advanced Settings)中配置回调URL(如下图所示)。

 大部分发行商只使用 %user%,可以加%txid%和 %digest%来进行安全设置,但其实以下所有变量都是可用的:

变量描述
%user% 通过以下平台提供给Vungle SDK的用户名:
iOS- 传递options dict 中 VunglePlayAdOptionKeyUser 参数到方法playAd()
Android- 传递global ad config object 中的setIncentivizedUserId 参数到方法 playAd()
%udid% 设备的唯一标识符
%ifa%

iOS-苹果设备唯一标识符。

Android-该参数将会返回谷歌广告商ID(Google Advertiser ID)

%txid% 完整广告观看的唯一transaction ID
%digest% s验证回调来自Vungle的安全标志——详细信息参见安全部分

注意你只需要传递%user% 变量即可—如果将剩余变量包含至回调URL,那么这些变量将会从Vungle服务器提供。

b. 奖励配置

现在你已经可以为观看广告的用户发送奖励了,那么奖励他们什么呢?如果每次你都奖励宝石,那会很直接。但如果你想要给他们奖励一些更高级的东西怎么办?我们没有内置的奖励配置选项,但是我们推荐按照以下方法操作:

示例- 钱币 vs 生命:

假设你的应用在多个地方布置了奖励性广告——如应用商店及每第三次“游戏结束”。你想在商店里奖励钱币,而在每三次“游戏结束”后奖励一条生命。对每个playAd(),你都需要按照如下方式配置user参数:

userName123:coins 或者 userName123:lives

如此,当你的服务器接收到Vungle的回调时,解析 %user% 得到正确的奖励!

c.安全

验证回调

为了验证你接收到的回调是否来自于Vungle,打开高级设置(Advanced Settings),为应用选择“Secret Key for Secure Callback”复选框。这样便会产生一个如下所示的密钥(secret key):

4YjaiIualvm8/4wkMBRH8pctlqB1NyzhK3qUGUar+Zc=

 

你可以使用这个密钥按照如下步骤来验证回调的来源:

  1. 通过连接(concatenate)密钥和transaction ID创建原始交易验证字符串,密钥和transaction ID以冒号分开,如下所示 transactionString = secretKey + ":" + %txid%
  2. 利用SHA-256算法对transactionString 的字节进行两次散列。
  3. 对连续两轮SHA-256散列的输出字节进行十六进制编码: transactionToken = 870a0d13da1f1670b5ba35f27604018aeb804d5e6ac5c48194b2358e6748e2a8
  4. 检查产生的 transactionToken 是否与回调中发送的querystring如 %digest%相等。

防止重放攻击

为防止单个回调在你的服务器是重放多次,你需要保存验证过的transaction ID并拒绝后面所有带有相同transaction ID的回调。因为一个transaction ID包含一个时间戳,所以你可以通过这个来限制需要保存transaction ID的数量。你需要通过时间执行截断,检查即使的回调步骤如下:

  1. 从transaction 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) {
  // 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
还有其它问题?提交请求

评论