Skip to content

激励视频服务端验证

1.激励视频服务端验证介绍

服务器端验证 (Server-side verification) 是对应用内激励视频广告观看行为进行的额外验证,可规避欺骗客户端回调来奖励用户的行为。 您可以使用服务器端验证对每一次激励视频广告观看行为进行验证,每次用户看完激励视频广告之后,ToBid 都会使用您在设置聚合广告位时提供的回传网址对此次观看行为进行信息回传。开发者可参考服务端验证信息,自行判断进行奖励的下发。

激励视频服务端验证功能需要 ToBid SDK 版本>=2.0.0

2.ToBid 服务器端验证使用流程

使用服务端激励验证,仅需以下步骤:

  1. 前往【聚合管理】-【流量管理】-点击应用进入【聚合广告列表】, 点击编辑激励视频聚合广告位
  2. 打开【服务器回调】开关,填写并保存激励服务端回调相关参数,例如回调地址等。若开发者需要通过回调回传 user_id自定义用户数据等信息,可参考此文档 SDK 参数传入模块。
  3. 用户使用 ToBid SDK 2.0.0 及其以上版本观看完成激励视频后,ToBid 服务端将通过平台设置的回调地址通知开发者。开发者可以通过服务端回调中的信息,自行判断是否下发奖励

2.1 服务端验证平台字段说明

在编辑激励视频聚合广告位页面,开发者可以编辑如图所示的信息。

回调url处,可填写完整回调url,ToBid将自动解析对应宏参数。您也可单独输入回调地址前半部分,与ToBid相关参数在下方进行手动添加。

图片名称
  • 回调 URL: ToBid 在用户看完激励广告后,以 GET 方式给开发者通知的地址。例如:
http://www.mysite.com/granting.php?appUserId={{USER_ID}}&rewardName={{REWARD_NAME}}&rewardAmount={{REWARD_AMOUNT}}&transId={{TRANS_ID}}&placementId={{PLACEMENT_ID}}&networkId={{NETWORK_ID}}&aggrplacementId={{AGGR_PLACEMENT_ID}}&sign={{SIGN}}&extraInfo={{EXTRAINFO}}&price={{PRICE}}

注意

{} 括号中的值是宏字段,实际的请求中 ToBid 均会替换真实字段;url中需包含所有宏,但值都由开发者自定义,没有值可以传空

  • 奖励名称: 聚合广告位设置的奖品名称;例如:金币、积分等。
  • 奖励数量: 聚合广告位设置的奖励数量。
  • Security Key: 密钥。点击"生成",系统会更新密钥(谨慎点击)。该字段用于生成回调验证签名,以及对价格进行加密,具体可参见服务端验证回传参数说明 中关于签名{{SIGN}}和价格{{PRICE}}的字段说明

2.2 服务端验证回传参数说明

ToBid 服务端验证目前支持以下信息,开发者回调地址中需要包含对应的参数宏。

参数宏参数类型说明
{{USER_ID}}string开发者应用的用户id,可通过SDK 激励视频的 user_id 字段传入,使用激励视频服务端验证,该字段必传。客户端传入方法 iOS参考安卓参考
{{TRANS_ID}}string服务端生成的trans_id,具有唯一性,用于标识此次广告观看行为。 开发者可通过此 id 对奖励 发放行为进行去重校验,避免重复发放奖励
{{REWARD_AMOUNT}}int奖励数量 ,开发者在编辑激励视频服务端回调信息的页面填写的奖励数量。
{{REWARD_NAME}}string奖励名称 ,开发者在编辑激励视频服务端回调信息的页面填写的奖励名称。
{{PLACEMENT_ID}}stringToBid 聚合广告位id,观看此次激励视频的聚合广告位id
{{NETWORK_ID}}string此次激励视频所属的广告平台,广告平台对应的ID参考
{{AGGR_PLACEMENT_ID}}string此次激励视频所属的广告平台的代码位id。注意,此字段仅传输唯一代码位id,对于 Mintegral 渠道,回传 unitId
{{PRICE}}string当次广告播放的 排序价格 。仅 SDK 3.0.0 版本及其以上支持,其他版本替换为空字符。价格字段需要先使用 urldecode后,再使用base64 decode,最后使用 AES256 算法加解密,密钥为对应聚合广告位的Security Key,填充方式:pkcs5。币种:CNY,单位分,具体代码示例可参考 2.5 价格解密示例
{{SIGN}}string签名校验。为了保证数据的安全性,平台回调参数中包含了签名信息,签名方式是将激励回调设置中的Security Key加上{{TRANS_ID}},中间以冒号分隔(伪代码:securityKey +":"+trans_id),对拼接完成后的字符串进行sha256摘要运算。这样可以确保trans_id的真实性。建议开发者同时实现trans_id去重校验能力,以此来提高奖励发放的安全性。
{{EXTRAINFO}}string通过 SDK的WindMillAdRequest类的options属性传入,options是个字典,key和value都是媒体自定义的,具体代码可参考
{{THIRD_TRANS_ID}}string三方广告激励回调中transid,依赖SDK3.3.0及以上版本。当前支持广告网络:百度联盟、优量汇
{{NETWORK_AD_TYPE}}int三方广告源的原始广告类型。
1 代表激励视频
2 代表开屏广告
4 代表插屏广告
5 代表原生广告
7 代表Banner广告
依赖SDK3.9.0及以上版本
{{REWARD_TIMING}}int激励发放时机。
0 默认激励发放时机;(三方广告源为激励视频类型时,依赖三方激励回调接口发放奖励)
1 广告关闭时机;(仅支持多类型优选-三方广告源为插屏或开屏时,广告关闭时机发放奖励)
2 广告点击时机;(仅支持多类型优选-三方广告源为插屏或开屏时,广告点击时机发放奖励)
依赖SDK3.9.0及以上版本
{{THIRD_CODE_PRICE}}string三方渠道的加密价格(当前支持百度渠道)
依赖SDK4.0.1及以上版本
{{APP_VERSION}}string开发者应用版本号
iOS SDK4.1.0版本支持;Android SDK4.3.0版本支持
{{SDK_VERSION}}string聚合sdk版本号

2.3 签名校验生成示例

sha256工具类(Java版本)

引入maven

xml
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
<dependency></dependency>
Java
import org.apache.commons.codec.binary.Hex;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class Sha256Util {
  /***
   * 利用Apache的工具类实现SHA-256加密
   *
   * @param str 加密的报文
   * @return String
   */
  public static String getSHA256Str(String str) {
    MessageDigest messageDigest;
    String encodeStr = "";
    try {
      messageDigest = MessageDigest.getInstance("SHA-256");
      byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));
      encodeStr = Hex.encodeHexString(hash);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return encodeStr;
  }
}

使用:

securityKey 取自聚合广告位服务端回调设置中的Security Key参见 服务端验证平台字段说明

Java
String signString = securityKey + ":" + trans_id;
Sha256Util.getSHA256Str( signString );

2.4 回调响应

如果回调通过了开发者的全部验证,开发者需要返回以下JSON数据:

json
"isValid":true
字段定义字段值或释义字段类型备注
isValidtrue/falseBOOL开发者服务端收到请求后判断回调结果,返回给 ToBid 服务器的校验结果。

如果因为网络原因没有收到回调响应,ToBid 会每隔200毫秒重试3次。

2.5 价格解密示例

解密过程说明

开发者需要对收到的价格参数进行解密处理:

1、首先执行urldecode操作

2、使用Base64进行decode操作

3、使用AES/ECB/PKCS5Padding算法对字符串进行解密。解密使用的key取自聚合广告位服务端回调设置中的Security Key参见 服务端验证平台字段说明

Java 示例

Java
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class TestAes {
	public static final String ALGORITHM = "AES";
	public static final String SECRET_AES_ECB_MODE = "ECB";
	public static final String SECRET_AES_PKCS5_PADDING = "PKCS5Padding";

	/**
	 * 解密
	 *
	 * <p>
	 * byte[] -> byte[]
	 *
	 * @param mode       模式
	 * @param padding    填充
	 * @param key        密钥
	 * @param iv         向量,如果模式不用向量,可传递null
	 * @param ciphertext 密文
	 * @return 明文
	 */
	public static byte[] decrypt(String mode, String padding, byte[] key, byte[] iv, byte[] ciphertext) {
		return doBytes(mode, padding, key, iv, ciphertext, Cipher.DECRYPT_MODE);
	}

	private static byte[] doBytes(String mode, String padding, byte[] key, byte[] iv, byte[] input, int opMode) {
		Cipher cipher = createCipher(mode, padding, key, iv, opMode);
		try {
			byte[] output = cipher.doFinal(input);
			return output;
		} catch (IllegalBlockSizeException | BadPaddingException e) {
			throw new RuntimeException(e);
		}
	}

	private static Cipher createCipher(String mode, String padding, byte[] key, byte[] iv, int opMode) {
		SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
		IvParameterSpec ivSpec = null;
		if (iv != null) {
			ivSpec = new IvParameterSpec(iv);
		}

		String transformation = ALGORITHM + "/" + mode + "/" + padding;
		Cipher cipher;
		try {
			cipher = Cipher.getInstance(transformation);
			cipher.init(opMode, keySpec, ivSpec);
			return cipher;
		} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
				| InvalidAlgorithmParameterException e) {
			throw new RuntimeException(e);
		}
	}

	public static void main(String[] args) {

		// 原始价格,单位为分
		String price = "1000";

		// 聚合广告位 Security Key
		String secretKey = "xm3k88pem11a5cdg7gqn8agoxnh328m9";

		// 平台使用上述key,对price加密、base64 encode、url encode之后得到的加密价格串
		String encryptedPrice = "1CIoSLK1Ye2SAAgNB5kiYQ%3D%3D";

		// 首先对收到的加密字符串进行 URL decode
		String decodedString = URLDecoder.decode(encryptedPrice, StandardCharsets.UTF_8);

		// 再对URL decode 之后的价格进行Base64 decode
		byte[] encryptedBytes = Base64.getDecoder().decode(decodedString.getBytes());

		// 进行解密
		byte[] decryptedBytes = TestAes.decrypt(SECRET_AES_ECB_MODE, SECRET_AES_PKCS5_PADDING, secretKey.getBytes(),
				null, encryptedBytes);

		String decryptedPrice = new String(decryptedBytes);

		// 解密后,价格应该等于1000
		System.out.println(price.equals(decryptedPrice));
	}
}

Python 示例

Python
import base64
import urllib.parse

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad


BLOCK_SIZE = 16

#价格明文
price = '1000'

#聚合广告位的Security Key
secret_key = b'xm3k88pem11a5cdg7gqn8agoxnh328m9'

#平台使用上述key,对price加密,base64 encode,url encode之后得到的加密价格串
encrypted_price = '1CIoSLK1Ye2SAAgNB5kiYQ%3D%3D'

#开始解密过程
#首先对加密串进行url decode
url_decoded_string = urllib.parse.unquote(encrypted_price)

#再对字符串进行base64 decode
base64_decoded_string = base64.b64decode(url_decoded_string)

#对字符串进行AES解密
f = AES.new(secret_key, mode=AES.MODE_ECB)
raw_res = unpad(f.decrypt(base64_decoded_string), BLOCK_SIZE).decode("utf-8")

print(raw_res)

PHP 示例

PHP
<?php

function pkcs7_pad($text, $blocksize)
{
    $pad = $blocksize - (strlen($text) % $blocksize);
    return $text . str_repeat(chr($pad), $pad);
}

function pkcs7_unpad($text)
{
    $pad = ord($text[strlen($text) - 1]);
    if ($pad > strlen($text)) return false;
    if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) return false;
    return substr($text, 0, -1 * $pad);
}

function decryptData($input,$key){
        $cipher = "aes-256-ecb";
        $output=@openssl_decrypt($input, $cipher , $key);
        if($output===FALSE)
            echo "decrypt data error";
        return $output;
}

if (!in_array('aes-256-ecb',openssl_get_cipher_methods())){
    echo "please install openssl_encrypt\n e.g.:https://www.php.net/manual/zh/book.openssl.php\n";
    die;
}

$secret_key='xm3k88pem11a5cdg7gqn8agoxnh328m9';

//平台加密后的价格
$ency_text='1CIoSLK1Ye2SAAgNB5kiYQ%3D%3D';

//解密后,价格应该等于1000
echo "ori_price : ".decryptData(urldecode($ency_text),$secret_key)."\n";

?>

3.服务器端验证 SDK 相关参数使用说明

使用激励视频服务端验证时,开发者需要按照要求在平台上做相应的参数配置,并且通过以下方法传入对应的宏信息。 客户端本地提供的 Trans_ID,可用于校验和服务端回调信息的一致性。开发者可根据需求使用。

3.1 iOS

开发者可通过以下方法传入 user_id 以及一些自定义信息

objective-c
WindMillAdRequest *request = [WindMillAdRequest request];
request.userId = @"user_id";
request.placementId = @"ea1f8f7b662";
request.options = @{@"test_key":@"test_value"};//s2s激励时自定义参数,对应{{EXTRAINFO}}
WindMillRewardVideoAd *rewardVideoAd = [[WindMillRewardVideoAd alloc] initWithRequest:request];
rewardVideoAd.delegate = self;
[rewardVideoAd loadAdData];

激励视频服务端验证业务中,ToBid 服务端会向开发者服务端发送一个验证请求,同时客户端会给出 - (void)rewardVideoAd:(WindMillRewardVideoAd *)rewardVideoAd reward:(WindMillRewardInfo *)reward 回调,开发者可根据此回调获取服务端回调的 Trans_ID。

注意:在满足激励条件时会触发此回调,与是否选择服务端验证以及服务端验证是否成功无关。

3.2 安卓

开发者可通过以下方法传入 user_id 以及一些自定义信息

java
private WMRewardAd rewardVideoAd;

/**
 * PLACEMENT_ID 必填
 * USER_ID 可选
 * OPTIONS 自定义参数,对应服务端回调的{{EXTRAINFO}}
 */
WMRewardAdRequest request = new WMRewardAdRequest(PLACEMENT_ID, USER_ID, OPTIONS);

激励视频服务端验证业务中,ToBid 服务端会向开发者服务端发送一个验证请求,同时客户端会给onVideoRewarded(AdInfo adInfo, WMRewardInfo rewardInfo) 回调,开发者可根据此回调获取服务端回调的 Trans_ID。

4.附录

ToBid 平台支持的广告网络对应 NetworkID 列表

广告网络广告网络Id
Mintegral1
Vungle4
Applovin5
Unityads6
ironSource7
Sigmob9
Admob11
Tapjoy12
Toutiao13
FaceBook15
gdt16
Kuaishou19
Klevin20
Baidu21
Gromore22
Huawei25
AdScope27
qumeng28
TapADN29
Pangle30
reklamup33
oppo adn34
honor36
inmobi37
ToBid ADX999