服务器端验证回调是包含查询参数的网址请求 这些请求会由 Google 发送到外部系统 通知它用户应该因与激励广告互动而获得奖励,或 插页式激励广告。激励广告 SSV(服务器端验证)回调 可针对客户端回调的仿冒提供额外一层保护 来奖励用户
本指南介绍了如何使用 Tink Java Apps 第三方 加密库,以确保回调中的查询参数 合理价值。 虽然本指南在介绍时使用的是 Tink,但您可以选择 使用任何支持 ECDSA。 您也可以通过测试 工具。
查看此功能完全正常运行 示例 使用 Java Spring-boot
前提条件
将激励广告集成到您的 移动应用 Google 移动广告 SDK v9.1.0 或更高版本。
启用激励广告服务器端 验证。
使用 Tink Java Apps 库中的 RewardedAdsVerifier
Tink Java Apps GitHub 代码库
包含一个
RewardedAdsVerifier
辅助类来减少验证激励广告 SSV 回调所需的代码。
通过使用此类,您可以使用以下代码验证回调网址。
RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder()
.fetchVerifyingPublicKeysWith(
RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
.build();
String rewardUrl = ...;
verifier.verify(rewardUrl);
如果 verify()
方法执行顺利,未发生任何异常,则回调
已成功验证网址。奖励用户
部分详细介绍了应在何时奖励用户的最佳做法。对于
此类为验证激励广告 SSV 回调而执行的步骤明细。
您可以仔细阅读手动验证激励广告
SSV 部分。
SSV 回调参数
服务器端验证回调包含查询参数,用于描述 激励广告互动。参数名称、说明和示例值包括 。参数按字母顺序发送。
参数名称 | 说明 | 示例值 |
---|---|---|
ad_network | 促成此广告的广告来源的广告来源标识符。广告来源 与 ID 值相对应的名称列在广告 来源标识符部分。 | 1953547073528090325 |
ad_unit | 用于请求激励广告的 AdMob 广告单元 ID。 | 2747237135 |
custom_data | 自定义数据字符串,由
ServerSideVerificationOptions::custom_data
。
如果应用未提供自定义数据字符串,则此查询参数 值将不会出现在 SSV 回调中。 |
SAMPLE_CUSTOM_DATA_STRING |
key_id | 用于验证 SSV 回调的密钥。此值映射到一个公钥 AdMob 密钥服务器提供的密钥。 | 1234567890 |
reward_amount | 广告单元设置中指定的奖励金额。 | 5 |
reward_item | 广告单元设置中指定的奖品。 | 金币 |
signature | AdMob 生成的 SSV 回调的签名。 | MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY |
timestamp | 用户获奖时间戳(以毫秒为单位的 Epoch 时间)。 | 1507770365237823 |
transaction_id | AdMob 为每个奖励授予事件生成的唯一的十六进制编码标识符。 | 18fa792de1bca816048293fc71035638 |
user_id | 用户标识符
ServerSideVerificationOptions::user_id 。
如果应用未提供用户标识符,此查询参数将不会 出现在 SSV 回调中。 |
1234567 |
广告来源标识符
广告来源名称和 ID
广告来源名称 | 广告来源 ID |
---|---|
Aarki(出价) | 5240798063227064260 |
Ad Generation(出价) | 1477265452970951479 |
AdColony | 15586990674969969776 |
AdColony(非 SDK)(出价) | 4600416542059544716 |
AdColony(出价) | 6895345910719072481 |
AdFalcon | 3528208921554210682 |
AdMob 广告联盟 | 5450213213286189855 |
AdMob 广告联盟广告瀑布流 | 1215381445328257950 |
ADResult | 10593873382626181482 |
AMoAd | 17253994435944008978 |
AppLovin | 1063618907739174004 |
AppLovin(出价) | 1328079684332308356 |
Chartboost | 2873236629771172317 |
Chocolate Platform(出价) | 6432849193975106527 |
跨渠道 (MdotM) | 9372067028804390441 |
自定义事件 | 18351550913290782395 |
DT Exchange* * 在 2022 年 9 月 21 日之前,该广告联盟称为“Fyber Marketplace”。 | 2179455223494392917 |
EMX(出价) | 8497809869790333482 |
Fluct(出价) | 8419777862490735710 |
小风 | 3376427960656545613 |
Fyber* * 此广告来源用于生成历史报告。 | 4839637394546996422 |
i-mobile | 5208827440166355534 |
优化数字化(出价) | 159382223051638006 |
Index Exchange(出价) | 4100650709078789802 |
InMobi | 7681903010231960328 |
InMobi(出价) | 6325663098072678541 |
InMobi Exchange(出价) | 5264320421916134407 |
IronSource | 6925240245545091930 |
ironSource Ads(出价) | 1643326773739866623 |
Leadbolt | 2899150749497968595 |
LG U+AD | 18298738678491729107 |
LINE 广告联盟 | 3025503711505004547 |
maio | 7505118203095108657 |
maio(出价) | 1343336733822567166 |
Media.net(出价) | 2127936450554446159 |
参与中介的自家广告 | 6060308706800320801 |
Meta Audience Network* * 在 2022 年 6 月 6 日之前,该广告联盟称为“Facebook Audience Network”。 | 10568273599589928883 |
Meta Audience Network(出价)* * 在 2022 年 6 月 6 日之前,该广告联盟称为“Facebook Audience Network(出价)”。 | 11198165126854996598 |
Mintegral | 1357746574408896200 |
Mintegral(出价) | 6250601289653372374 |
MobFox | 8079529624516381459 |
MobFox(出价) | 3086513548163922365 |
MoPub(已弃用) | 10872986198578383917 |
myTarget | 8450873672465271579 |
Nend | 9383070032774777750 |
Nexxen(出价)* * 在 2024 年 5 月 1 日之前,该广告联盟称为“UnrulyX”。 | 2831998725945605450 |
ONE by AOL (Millennial Media) | 6101072188699264581 |
ONE by AOL (Nexage) | 3224789793037044399 |
OneTag Exchange(出价) | 4873891452523427499 |
OpenX(出价) | 4918705482605678398 |
邦格尔 | 4069896914521993236 |
Pangle(出价) | 3525379893916449117 |
PubMatic(出价) | 3841544486172445473 |
预订型广告系列 | 7068401028668408324 |
RhythmOne(出价) | 2831998725945605450 |
Rubicon(出价) | 3993193775968767067 |
SK 星球 | 734341340207269415 |
Sharethrough(出价) | 5247944089976324188 |
Smaato(出价) | 3362360112145450544 |
Equativ(出价)* * 在 2023 年 1 月 12 日之前,该广告联盟称为“Smart Adserver”。 | 5970199210771591442 |
Sonobi(出价) | 3270984106996027150 |
Tapjoy | 7295217276740746030 |
Tapjoy(出价) | 4692500501762622178 |
Tencent GDT | 7007906637038700218 |
TripleLift(出价) | 8332676245392738510 |
Unity 广告 | 4970775877303683148 |
Unity Ads(出价) | 7069338991535737586 |
Verizon Media | 7360851262951344112 |
Verve Group(出价) | 5013176581647059185 |
Vpon | 1940957084538325905 |
Liftoff Monetize* * 在 2023 年 1 月 30 日之前,该广告联盟称为“Vungle”。 | 1953547073528090325 |
Liftoff Monetize(出价)* * 在 2023 年 1 月 30 日之前,该广告联盟称为“Vungle(出价)”。 | 4692500501762622185 |
Yieldmo(出价) | 4193081836471107579 |
YieldOne(出价) | 3154533971590234104 |
Zucks | 5506531810221735863 |
奖励用户
在决定采用哪种方法时,务必要在用户体验和奖励验证之间取得平衡 何时奖励用户服务器端回调 访问外部系统。因此,建议的最佳做法是使用 客户端回调以立即奖励用户,同时执行 在收到服务器端回调时对所有奖励进行验证。这个 方法不仅能提供良好的用户体验,还能确保 奖励。
不过,对于对奖励有效性至关重要的应用(例如, 奖励会影响应用的游戏内经济),且奖励延迟会造成延迟 可接受,等待经过验证的服务器端回调可能是最佳选择 方法。
自定义数据
对于需要服务器端验证回调中额外数据的应用,应使用
激励广告的自定义数据功能。在激励广告上设置的任何字符串值
对象会传递给 SSV 回调的 custom_data
查询参数。如果拒绝
自定义数据值后,custom_data
查询参数值不会
。
以下代码示例演示了如何为激励广告设置自定义数据 对象。
firebase::gma::RewardedAd* rewarded_ad; rewarded_ad = new firebase::gma::RewardedAd(); firebase::gma::RewardedAd::ServerSideVerificationOptions options; options.custom_data = "SAMPLE_CUSTOM_DATA_STRING"; rewarded_ad->SetServerSideVerificationOptions(options);
如果您要设置自定义奖励字符串,则必须在展示之前设置 。
手动验证激励广告 SSV
RewardedAdsVerifier
类为验证激励广告而执行的步骤
下文概述了 SSV。尽管所包含的代码段是用 Java 编写的,
利用 Tink 第三方库,您可以
您选择的语言,使用任何支持
ECDSA。
提取公钥
若要验证激励广告 SSV 回调,您需要一个由 AdMob 提供的公钥。
您可以使用公钥列表验证激励广告 SSV 回调, 从 AdMob 键获取 服务器。公钥列表 以 JSON 表示形式提供,格式类似于以下内容:
{
"keys": [
{
keyId: 1916455855,
pem: "-----BEGIN PUBLIC KEY-----\nMF...YTPcw==\n-----END PUBLIC KEY-----"
base64: "MFkwEwYHKoZIzj0CAQYI...ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw=="
},
{
keyId: 3901585526,
pem: "-----BEGIN PUBLIC KEY-----\nMF...aDUsw==\n-----END PUBLIC KEY-----"
base64: "MFYwEAYHKoZIzj0CAQYF...4akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw=="
},
],
}
要获取公钥,请连接到 AdMob 密钥服务器并下载
键。以下代码可完成此任务并保存 JSON
data
变量的键表示法。
String url = ...;
NetHttpTransport httpTransport = new NetHttpTransport.Builder().build();
HttpRequest httpRequest =
httpTransport.createRequestFactory().buildGetRequest(new GenericUrl(url));
HttpResponse httpResponse = httpRequest.execute();
if (httpResponse.getStatusCode() != HttpStatusCodes.STATUS_CODE_OK) {
throw new IOException("Unexpected status code = " + httpResponse.getStatusCode());
}
String data;
InputStream contentStream = httpResponse.getContent();
try {
InputStreamReader reader = new InputStreamReader(contentStream, UTF_8);
data = readerToString(reader);
} finally {
contentStream.close();
}
请注意,公钥会经常轮换。您会收到一封电子邮件, 为您介绍即将进行的轮替。如果您要缓存公钥 获悉密钥。
提取公钥后,必须对其进行解析。通过
下面的 parsePublicKeysJson
方法接受 JSON 字符串,例如示例
作为输入,它会创建从 key_id
值到公钥的映射,
它们封装为 Tink 库中的 ECPublicKey
对象。
private static Map<Integer, ECPublicKey> parsePublicKeysJson(String publicKeysJson)
throws GeneralSecurityException {
Map<Integer, ECPublicKey> publicKeys = new HashMap<>();
try {
JSONArray keys = new JSONObject(publicKeysJson).getJSONArray("keys");
for (int i = 0; i < keys.length(); i++) {
JSONObject key = keys.getJSONObject(i);
publicKeys.put(
key.getInt("keyId"),
EllipticCurves.getEcPublicKey(Base64.decode(key.getString("base64"))));
}
} catch (JSONException e) {
throw new GeneralSecurityException("failed to extract trusted signing public keys", e);
}
if (publicKeys.isEmpty()) {
throw new GeneralSecurityException("No trusted keys are available.");
}
return publicKeys;
}
获取要验证的内容
激励广告 SSV 回调的最后两个查询参数始终为 signature
和 key_id,
。其余查询参数会指定
进行验证。我们假设您已将 AdMob 配置为向
https://www.myserver.com/mypath
。以下代码段展示了一个激励广告示例
突出显示待验证内容的 SSV 回调。
https://www.myserver.com/path?ad_network=54...55&ad_unit=12345678&reward_amount=10&reward_item=coins ×tamp=150777823&transaction_id=12...DEF&user_id=1234567&signature=ME...Z1c&key_id=1268887
以下代码演示了如何通过 作为 UTF-8 字节数组的回调网址。
public static final String SIGNATURE_PARAM_NAME = "signature=";
...
URI uri;
try {
uri = new URI(rewardUrl);
} catch (URISyntaxException ex) {
throw new GeneralSecurityException(ex);
}
String queryString = uri.getQuery();
int i = queryString.indexOf(SIGNATURE_PARAM_NAME);
if (i == -1) {
throw new GeneralSecurityException("needs a signature query parameter");
}
byte[] queryParamContentData =
queryString
.substring(0, i - 1)
// i - 1 instead of i because of & in the query string
.getBytes(Charset.forName("UTF-8"));
从回调网址获取签名和 key_id
使用上一步中的 queryString
值,解析 signature
并
key_id
查询参数,如下所示:
public static final String KEY_ID_PARAM_NAME = "key_id=";
...
String sigAndKeyId = queryString.substring(i);
i = sigAndKeyId.indexOf(KEY_ID_PARAM_NAME);
if (i == -1) {
throw new GeneralSecurityException("needs a key_id query parameter");
}
String sig =
sigAndKeyId.substring(
SIGNATURE_PARAM_NAME.length(), i - 1 /* i - 1 instead of i because of & */);
int keyId = Integer.valueOf(sigAndKeyId.substring(i + KEY_ID_PARAM_NAME.length()));
执行验证
最后一步是使用
相应的公钥。获取从
parsePublicKeysJson
方法并使用回调中的 key_id
参数
用于从该映射中获取公钥的网址。然后使用以下应用验证签名:
该公钥。下面的 verify
方法演示了这些步骤。
private void verify(final byte[] dataToVerify, int keyId, final byte[] signature)
throws GeneralSecurityException {
Map<Integer, ECPublicKey> publicKeys = parsePublicKeysJson();
if (publicKeys.containsKey(keyId)) {
foundKeyId = true;
ECPublicKey publicKey = publicKeys.get(keyId);
EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, HashType.SHA256, EcdsaEncoding.DER);
verifier.verify(signature, dataToVerify);
} else {
throw new GeneralSecurityException("cannot find verifying key with key ID: " + keyId);
}
}
如果方法执行时未引发异常,则回调网址 已成功通过验证。
常见问题解答
- 我可以缓存 AdMob 密钥服务器提供的公钥吗?
- 我们建议您缓存 AdMob 密钥提供的公钥 可减少验证 SSV 所需的操作次数 回调。但请注意,公钥会经常轮换,因此不应 缓存超过 24 小时。
- AdMob 密钥服务器提供的公钥多久轮替一次?
- AdMob 密钥服务器提供的公钥会基于变量进行轮替 时间表。为确保可继续验证 SSV 回调, 因此公钥的缓存时间不应超过 24 小时。
- 如果无法访问我的服务器,会发生什么情况?
- Google 预计您的 SSV 收到
HTTP 200 OK
成功状态响应代码 回调。如果您的服务器无法访问或未提供预期的 响应中,Google 会在 为 1 秒。 - 如何验证 SSV 回调是否来自 Google?
- 使用 DNS 反向查找来验证 SSV 回调是否来自 Google。