服务器端验证回调是包含查询参数的网址请求 这些请求会由 Google 发送到外部系统 通知它用户应该因与激励广告互动而获得奖励,或 插页式激励广告。激励广告 SSV(服务器端验证)回调 可针对客户端回调的仿冒提供额外一层保护 来奖励用户
本指南介绍了如何使用 Tink Java Apps 第三方 加密库,以确保回调中的查询参数 合理价值。 虽然本指南在介绍时使用的是 Tink,但您可以选择 使用任何支持 ECDSA。 您也可以通过测试 工具。
查看此功能完全正常运行 示例 使用 Java Spring-boot
前提条件
将激励广告集成到您的 移动应用 v3.12.0 或更高版本的 Google 移动广告 Unity 插件。
启用激励广告服务器端 验证。
使用 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 |
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 | 用户标识符
SetUserId 。
如果应用未提供用户标识符,此查询参数将不会 出现在 SSV 回调中。 |
1234567 |
广告来源标识符
广告来源名称和 ID
广告来源名称 | 广告来源 ID |
---|---|
Aarki(出价) | 5240798063227064260 |
Ad Generation(出价) | 1477265452970951479 |
AdColony | 15586990674969969776 |
AdColony(非 SDK)(出价) | 4600416542059544716 |
AdColony(出价) | 6895345910719072481 |
AdFalcon | 3528208921554210682 |
AdMob 广告联盟 | 5450213213286189855 |
ADResult | 10593873382626181482 |
AMoAd | 17253994435944008978 |
AppLovin | 1063618907739174004 |
AppLovin(出价) | 1328079684332308356 |
查特波斯特 | 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 |
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 |
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 |
弗蓬 | 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
查询参数值不会
。
以下代码示例演示了如何在 激励广告。
private void LoadRewardedAd(string adUnitId)
{
// Send the request to load the ad.
AdRequest adRequest = new AdRequest();
RewardedAd.Load(adUnitId, adRequest, (RewardedAd rewardedAd, LoadAdError error) =>
{
// If the operation failed with a reason.
if (error != null)
{
Debug.LogError("Rewarded ad failed to load an ad with error : " + error);
return;
}
var options = new ServerSideVerificationOptions
.Builder()
.SetCustomData("SAMPLE_CUSTOM_DATA_STRING")
.Build()
rewardedAd.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。