服务器端验证回调是 Google 发送给外部系统的网址请求,其中带有 Google 扩展的查询参数,用于通知外部系统某位用户因为与激励广告或插页式激励广告互动而应予以奖励。激励广告 SSV(服务器端验证)回调提供了额外的保护层,可规避通过欺骗客户端回调来奖励用户的行为。
本指南介绍如何使用 Tink Java Apps 第三方加密库来验证激励广告 SSV 回调,以确保回调中的查询参数均属合法值。虽然本指南在介绍时使用的是 Tink,但您可以选择使用任何支持 ECDSA 的第三方库。您还可以使用 AdMob 界面中的测试工具测试服务器。
查看使用 Java Spring-boot 的这个完全正常运行的示例。
前提条件
将激励广告植入到移动应用中,并安装Google 移动广告 SDK v7.28.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 | 自定义数据字符串,提供方:
customRewardString
。
如果应用未提供自定义数据字符串,此查询参数值将不会出现在 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 |
时间戳 | 用户获奖时间戳(以毫秒为单位的纪元时间)。 | 1507770365237823 |
transaction_id | AdMob 为每个奖励授予事件生成的唯一的十六进制编码标识符。 | 18fa792de1bca816048293fc71035638 |
user_id | 用户标识符,由
userIdentifier 。
如果应用未提供用户标识符,此查询参数将不会出现在 SSV 回调中。 |
1234567 |
广告来源标识符
广告来源名称和 ID
اسم مصدر الإعلان | رقم تعريف مصدر الإعلان |
---|---|
Aarki (عروض الأسعار) | 5240798063227064260 |
إنشاء الإعلانات (عروض الأسعار) | 1477265452970951479 |
AdColony | 15586990674969969776 |
AdColony (غير مستندة إلى حزمة تطوير البرامج (SDK)) (عروض الأسعار) | 4600416542059544716 |
AdColony (عروض الأسعار) | 6895345910719072481 |
AdFalcon | 3528208921554210682 |
شبكة AdMob | 5450213213286189855 |
ADResult | 10593873382626181482 |
AMoAd | 17253994435944008978 |
أبلوفين | 1063618907739174004 |
Apploin (عروض الأسعار) | 1328079684332308356 |
كشافة الرسم البياني | 2873236629771172317 |
منصّة الشوكولاتة (عروض الأسعار) | 6432849193975106527 |
قناة CrossChannel (MdotM) | 9372067028804390441 |
حدث مخصّص | 18351550913290782395 |
DT Exchange* * قبل 21 أيلول (سبتمبر) 2022، كان يُطلق على هذه الشبكة اسم "سوق Fyber". | 2179455223494392917 |
EMX (عروض الأسعار) | 8497809869790333482 |
متقلّب (عروض الأسعار) | 8419777862490735710 |
Flurry | 3376427960656545613 |
Fyber* * يُستخدَم مصدر الإعلان هذا في إعداد التقارير السابقة. | 4839637394546996422 |
i-mobile | 5208827440166355534 |
التحسين الرقمي (عروض الأسعار) | 159382223051638006 |
بورصة الفهرس (عروض الأسعار) | 4100650709078789802 |
InMobi | 7681903010231960328 |
InMobi (عروض الأسعار) | 6325663098072678541 |
IronSource | 6925240245545091930 |
إعلانات ironSource (عروض الأسعار) | 1643326773739866623 |
Leadbolt | 2899150749497968595 |
LG U+AD | 18298738678491729107 |
شبكة LINE Ads | 3025503711505004547 |
مايو | 7505118203095108657 |
مايو (عروض الأسعار) | 1343336733822567166 |
Media.net (عروض الأسعار) | 2127936450554446159 |
إعلانات للشركة نفسها المعتمَدة على التوسّط | 6060308706800320801 |
Meta Audience Network* * قبل 6 حزيران (يونيو) 2022، كان يُطلق على هذه الشبكة اسم "Facebook Audience Network". | 10568273599589928883 |
Meta Audience Network (عروض الأسعار)* * قبل 6 حزيران (يونيو) 2022، كان يُطلق على هذه الشبكة اسم "Facebook Audience Network (عروض الأسعار)". | 11198165126854996598 |
نعناعي | 1357746574408896200 |
Mintegral (عروض أسعار) | 6250601289653372374 |
MobFox | 8079529624516381459 |
MobFox (عروض الأسعار) | 3086513548163922365 |
MoPub (متوقّف نهائيًا) | 10872986198578383917 |
myTarget | 8450873672465271579 |
Nend | 9383070032774777750 |
Nexxen (عرض أسعار)* * قبل 1 مايو 2024، كان يُطلق على هذه الشبكة اسم "UnrulyX". | 2831998725945605450 |
ONE by AOL (Millennial Media) | 6101072188699264581 |
ONE by AOL (Nexage) | 3224789793037044399 |
OneTag Exchange (عروض الأسعار) | 4873891452523427499 |
OpenX (عروض الأسعار) | 4918705482605678398 |
Pangle (عروض الأسعار) | 3525379893916449117 |
PubMatic (عروض الأسعار) | 3841544486172445473 |
حملة إعلانية قائمة على الحجوزات | 7068401028668408324 |
RhythmOne (عروض الأسعار) | 2831998725945605450 |
Rubicon (عروض الأسعار) | 3993193775968767067 |
كوكب ساسكاتشوان | 734341340207269415 |
نسبة مشاركة الإعلان بالكامل (عروض الأسعار) | 5247944089976324188 |
Smaato (عروض الأسعار) | 3362360112145450544 |
Equativ (عروض الأسعار)* * قبل 12 كانون الثاني (يناير) 2023، كان يُطلق على هذه الشبكة اسم "خادم الإعلانات الذكي". | 5970199210771591442 |
Sonobi (عروض الأسعار) | 3270984106996027150 |
Tapjoy | 7295217276740746030 |
Tapjoy (عروض الأسعار) | 4692500501762622178 |
تينسنت GDT | 7007906637038700218 |
TripleLift (عروض الأسعار) | 8332676245392738510 |
إعلانات Unity | 4970775877303683148 |
Unity Ads (عروض الأسعار) | 7069338991535737586 |
شركة Verizon Media | 7360851262951344112 |
مجموعة Verve (عروض الأسعار) | 5013176581647059185 |
فون | 1940957084538325905 |
Liftoff Monetize* * قبل 30 كانون الثاني (يناير) 2023، كان يُطلق على هذه الشبكة اسم "Vungle". | 1953547073528090325 |
Liftoff Monetize (عروض الأسعار)* * قبل 30 كانون الثاني (يناير) 2023، كان يُطلق على هذه الشبكة اسم "التفاعل (عروض الأسعار)". | 4692500501762622185 |
الأرباح (عروض الأسعار) | 4193081836471107579 |
YieldOne (عروض الأسعار) | 3154533971590234104 |
Zucks | 5506531810221735863 |
奖励用户
在决定何时奖励用户时,请务必在用户体验和奖励验证之间取得平衡。服务器端回调在到达外部系统之前可能会遇到延迟。因此,建议的最佳做法是使用客户端回调立即奖励用户,同时在收到服务器端回调时对所有奖励执行验证。这种方法既能提供良好的用户体验,又能确保所获奖励的有效性。
不过,对于奖励有效性至关重要(例如,奖励会影响应用的游戏内经济)且奖励授予延迟是可以接受的应用,等待经过验证的服务器端回调可能是最佳方法。
自定义数据
对于需要服务器端验证回调中额外数据的应用,应使用激励广告的自定义数据功能。在激励广告对象上设置的任何字符串值都将传递给 SSV 回调的 custom_data
查询参数。如果未设置自定义数据值,custom_data
查询参数值不会出现在 SSV 回调中。
以下代码示例演示了如何在加载激励广告后设置 SSV 选项。
Swift
GADRewardedAd.load(withAdUnitID:"ca-app-pub-3940256099942544/1712485313", request: request, completionHandler: { [self] ad, error in if let error != error { rewardedAd = ad let options = GADServerSideVerificationOptions() options.customRewardString = "SAMPLE_CUSTOM_DATA_STRING" rewardedAd.serverSideVerificationOptions = options }
Objective-C
GADRequest *request = [GADRequest request]; [GADRewardedAd loadWithAdUnitID:@"ca-app-pub-3940256099942544/1712485313" request:request completionHandler:^(GADRewardedAd *ad, NSError *error) { if (error) { // Handle Error return; } self.rewardedAd = ad; GADServerSideVerificationOptions *options = [[GADServerSideVerificationOptions alloc] init]; options.customRewardString = @"SAMPLE_CUSTOM_DATA_STRING"; ad.serverSideVerificationOptions = options; }];
手动验证激励广告 SSV
下面概述了 RewardedAdsVerifier
类为验证激励广告 SSV 而执行的步骤。尽管示例代码段使用的是 Java 语言,利用的是 Tink 第三方库,但您可通过支持 ECDSA 的任何第三方库,使用您选择的任何编程语言实施这些步骤。
提取公钥
如需验证激励广告 SSV 回调,您需要使用 AdMob 提供的公钥。
您可以从 AdMob 密钥服务器提取用于验证激励广告 SSV 回调的公钥列表。公钥列表以 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"));
从回调网址获取 signature 和 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);
}
}
如果方法执行顺利,未发生任何异常,则表示回调网址已验证成功。
FAQ
- 我可以缓存 AdMob 密钥服务器提供的公钥吗?
- 我们建议您缓存 AdMob 密钥服务器提供的公钥,以减少验证 SSV 回调所需的操作数量。但请注意,公钥会经常轮替,因此缓存时间不应超过 24 小时。
- AdMob 密钥服务器提供的公钥的轮替频率是多少?
- AdMob 密钥服务器提供的公钥会不定期轮替。为确保继续按预期验证 SSV 回调,公钥的缓存时间不应超过 24 小时。
- 如果无法访问我的服务器,会怎么样?
- Google 预计您的服务器会针对 SSV 回调返回
HTTP 200 OK
成功状态响应代码。如果您的服务器无法访问或未提供预期的响应,Google 将重新尝试发送 SSV 回调,每隔 1 秒发送最多 5 次。 - 如何验证 SSV 回调是否来自 Google?
- 使用 DNS 反向查找来验证 SSV 回调是否来自 Google。