为何使用奖励视频广告?
为最大化收入同时保持出色的 UX,Vungle 建议使用奖励视频广告。在应用的自然间隙显示,奖励视频广告可提供高收入,尤其是当您按照我们的建议让这些广告不可跳过时。此广告位置还提供出色的 UX,因为用户选择观看视频,然后被奖励一些有价值的物品,如虚拟货币、优质内容或应用内商品。向看完视频的用户发放的奖励的金额和类型完全由您决定。实现此目的有两种方法:应用内奖励或服务器到服务器回调。
建议选项:应用内奖励
概览
这是使用服务器到服务器回调的备选方法。当用户成功看完广告或点击下载按钮时,您可以在您的应用内直接奖励他们。这种方法的主要优点是实现起来非常简单。如果您更看重速度且不担心回播攻击,这是一个好方法。
Vungle 现在通过动态模板广告提供各种广告格式。与传统广告格式不同,此格式包含视频播放后跟一个结束卡,我们提供的模板在视频播放时会显示行动号召 (CTA) 按钮。看完视频广告的用户以及点击此按钮的用户应获得奖励。
注意:奖励广告有些情况下是指激励广告;这两个术语始终指的是同一类广告。在 SDK 代码和我们的报告 API 中,我们使用术语“激励”。
iOS 的实现
实现 VungleSDK Delegate,我们在 Vungle iOS v6.4.5 中进行了讨论。
- (void)vungleWillCloseAdWithViewInfo:(VungleViewInfo *)info placementID:(NSString *)placementID;
如果回调 vungleSDKwillCloseAdWithViewInfo
传递给您一个 viewInfo
字典,其密钥 completedView
或 didDownload
的值为‘yes’,说明用户已获得了奖励。
Android 的实现
实现 EventListener 接口,我们在 Vungle Android v6.4.11中进行了讨论。
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.x 注意事项:从 SDK v. 5.1 开始,我们将奖励选项移到了仪表板,这样发布人可以轻松更改此选项,而无需更改代码。在您的Dashboard中,设置奖励为奖励广告位即可。
Vungle SDK v.1.0 - v.4.1 注意事项:
- 对于 iOS,首先将您的
playAd
对象中的vunglePlayAdOptionKeyIncentivized
选项设置为‘yes’。 - 对于 Android,首先将您的
AdConfig
对象中的setIncentivizedFields选项设置为‘true’。
所有 Vungle SDK 版本的实现
当用户观看了 80% 或更多奖励广告内容时,即被视为完成了观看。Vungle 随后将使用callback URL 对您的服务器执行 ping 操作,此 URL 如下所示:
http://acme.com/bugzBunny/reward?uid=%user%
或像这样:
http://acme.com/bugzBunny/reward? amount=1&uid=%user%&txid=%txid%&digest=%digest%
我们建议使用etxid
和edigest
,像这样:
http://acme.com/bugzBunny/reward? amount=1&uid=%user%&etxid=%etxid%&edigest=%edigest%
.
请在仪表板上应用的设置中配置回调 URL(如下所示)。
大部分发布人只使用 %user%
、plus
%txid%
和 %digest%
来实现安全目的,但下面这些全部可以使用:
变量 |
说明 |
|
用户名通过以下方式提供给 Vungle SDK: ● iOS: ● Android:使用Vungle.setIncentivizedFields设值函数传递到 |
|
设备唯一识别码 |
|
● iOS:Apple 的设备唯一识别码。 ● Android:将返回 Google 广告客户 ID |
|
带服务器时戳的完成观看的唯一交易 ID;transactionID:timestamp(推荐)。 |
|
验证回调来自 Vungle 的安全令牌;请参阅安全部分了解详细信息(推荐)。 |
|
完成观看的唯一交易 ID |
|
验证回调来自 Vungle 的安全令牌;请参阅安全部分了解详细信息 |
请注意,%user%
是您需要传入的唯一变量。其余变量将来自 Vungle 服务器(如果您将它们放入回调 URL 中)。
奖励配置
您已经可以奖励观看广告的用户了,那么您用什么来作为奖品呢?如果您每次都奖励宝石,那再简单不过了。但是,如果您希望来点更高级的奖品该怎么办?我们没有为奖励配置提供内置选项,不过我们在这里给出了一些建议:
假设您的应用程序在多个位置放置了奖励广告:既在商店内,又在每三次“游戏结束”时。您希望在商店内奖励玩家硬币,在游戏结束时奖励生命。对于playAd()
的每个实例,请像这样配置用户:
userName123:coins or userName123:lives
然后,当您的服务器收到 Vungle 的回调时,会将 %user%
解析为正确的奖励!
安全
验证回调
为了验证您收到的回调来自 Vungle,请为您的应用程序选择“安全回调密钥”复选框。这会生成一个像下面这样的密钥:
4YjaiIualvm8/4wkMBRH8pctlqB1NyzhK3qUGUar+Zc=
您可以按照如下方法使用此密钥来验证回调的来源:
- 通过将密钥与交易 ID 连接来创建原始交易验证字符串,使用冒号分隔,像这样:
transactionString = secretKey + ":" + %txid% or %etxid%
- 使用 SHA-256 算法两次散列
transactionString
的字节。 - 通过对 SHA-256 散列的两个连续舍入的输入字节进行十六进制编码来生成交易验证令牌,看似下面这样:
transactionToken = 870a0d13da1f1670b5ba35f27604018aeb804d5e6ac5c48194b2358e6748e2a8
- 检查您生成的
transactionToken
是否与在回调查询字符串中作为以下对象发送的令牌相同:%digest%
或%edigest%
阻止重播攻击
若要阻止单个回调对您的服务器多次重播,请存储经过验证的交易 ID,并拒绝具有重复交易 ID 的未来回调。由于交易 ID 包含时戳,您可以通过固定及时回调的截止时间来限制必须存储的交易 ID 的数量,并检查是否存在重复 ID,方法如下:
- 从交易 ID 中提取时戳(以毫秒计),像这样:
transactionMillis = transactionId.substringAfter(":")
- 检查
transactionMillis
是否晚于您的截止时间,且在您的截止时间后未遇到该transactionId
。
示例代码
这些二进制数字的示例代码将帮助您实现服务器到服务器回调的安全!这里是一些示例:Node.js、Java、Python 和 Ruby。请注意:
- 此
transaction_id
可以是%txid%
或%etxid%
(推荐)。 - 对于
%etxid%
,验证哈希应为%edigest%
.
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) {
// For %etxid%, extract unique event id from etxid, which is the string before “:”, and use it to identify unique ad events
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
// For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events
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."""
"""For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events"""
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
# For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events
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