التحقق من استدعاءات التحقق من جانب الخادم (SSV)

استدعاءات إثبات الملكية من جانب الخادم هي طلبات عناوين URL، مع توسيع نطاق مَعلمات طلب البحث من خلال Google، والتي ترسلها Google إلى نظام خارجي لإعلامه بأنه يجب مكافأة المستخدم مقابل تفاعله مع إعلان بيني يضم مكافأة أو مكافأة. توفّر استدعاءات SSV (التحقُّق من جهة الخادم) بمكافأة طبقة حماية إضافية ضد انتحال هوية معاودة الاتصال من جهة العميل لمكافأة المستخدمين.

يوضِّح لك هذا الدليل كيفية التحقّق من عمليات استدعاء SSV بمكافأة باستخدام مكتبة التشفير التابعة لجهة خارجية في Tink Java Apps للتأكّد من أن معلَمات طلب البحث في استدعاء الدالة هي قيم سليمة. على الرغم من أنّ تطبيق Tink يُستخدم لأغراض هذا الدليل، يمكنك استخدام أي مكتبة تابعة لجهة خارجية تتيح استخدام ECDSA. ويمكنك أيضًا اختبار خادمك باستخدام أداة الاختبار في واجهة مستخدم AdMob.

راجع هذا المثال الذي يعمل بكامل طاقته باستخدام تمهيد Java النابض.

المتطلبات الأساسية

استخدام RewardedAdsVerifier من مكتبة تطبيقات Tink Java

يتضمّن مستودع Tink Java Apps في GitHub صف مساعد RewardedAdsVerifier لتقليل الرمز المطلوب لإثبات ملكية معاودة اتصال SSV بمكافأة. يتيح لك استخدام هذه الفئة إثبات ملكية عنوان URL لمعاودة الاتصال باستخدام الرمز التالي.

RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder()
    .fetchVerifyingPublicKeysWith(
        RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
    .build();
String rewardUrl = ...;
verifier.verify(rewardUrl);

إذا تم تنفيذ الطريقة verify() بدون رفع استثناء، يعني ذلك أنّه قد تم التحقّق من عنوان URL لمعاودة الاتصال بنجاح. ويوضّح قسم مكافأة المستخدم أفضل الممارسات المتعلقة بموعد مكافأة المستخدمين. لتحليل الخطوات التي ينفذها هذا الصف للتحقّق من عمليات استدعاء SSV التي تضم مكافأة، يمكنك قراءة القسم التحقق اليدوي من SSV الذي يضم مكافأة.

مَعلمات معاودة الاتصال في SSV

تحتوي استدعاءات إثبات الملكية من جانب الخادم على معلمات طلب بحث تصف التفاعل مع الإعلان بمكافأة. يتم سرد أسماء المعلمات والأوصاف وأمثلة القيم أدناه. يتم إرسال المَعلمات بترتيب أبجدي.

اسم المعلّمة الوصف مثال على القيمة
ad_network رقم تعريف مصدر الإعلان لمصدر الإعلان الذي استوفى هذا الإعلان. ويتم إدراج أسماء مصادر الإعلانات المقابلة لقيم أرقام التعريف في القسم معرّفات مصادر الإعلانات. 1953547073528090325
ad_unit رقم تعريف الوحدة الإعلانية في AdMob التي تم استخدامها لطلب الإعلان الذي يضم مكافأة. 2747237135
custom_data سلسلة بيانات مخصّصة على النحو المقدَّم من setCustomData .

إذا لم يوفّر التطبيق سلسلة بيانات مخصّصة، لن تتوفّر قيمة مَعلمة طلب البحث هذه في استدعاء SSV.

SAMPLE_CUSTOM_DATA_STRING
key_id مفتاح يتم استخدامه للتحقّق من معاودة الاتصال بـ SSV. ويتم ربط هذه القيمة بمفتاح عام يوفّره خادم مفتاح AdMob. 1234567890
reward_amount مبلغ المكافأة كما هو محدد في إعدادات الوحدة الإعلانية. 5
reward_item عنصر المكافأة كما هو محدد في إعدادات الوحدة الإعلانية. عملات معدنية
signature تم إنشاء توقيع لمعاودة الاتصال من خلال ميزة "التحقُّق بخطوتين" (SSV) التي تم إنشاؤها بواسطة AdMob. MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY
timestamp طابع زمني لوقت منح المستخدم كوقت Epoch بالمللي ثانية. 1507770365237823
transaction_id معرّف فريد بترميز سداسي لكل حدث منحة مكافأة ينشئه AdMob. 18fa792de1bca816048293fc71035638
user_id معرّف المستخدم على النحو المقدَّم من setUserId.

إذا لم يوفّر التطبيق أي معرّف مستخدم، لن تكون مَعلمة طلب البحث هذه متوفّرة في استدعاء SSV.

1234567

معرّفات مصادر الإعلانات

أسماء مصادر الإعلانات وأرقام تعريفها

广告来源名称 广告来源 ID
Aarki(出价)5240798063227064260
Ad Generation(出价)1477265452970951479
AdColony15586990674969969776
AdColony(非 SDK)(出价)4600416542059544716
AdColony(出价)6895345910719072481
AdFalcon3528208921554210682
AdMob 广告联盟5450213213286189855
ADResult10593873382626181482
AMoAd17253994435944008978
AppLovin1063618907739174004
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-mobile5208827440166355534
优化数字化(出价)159382223051638006
Index Exchange(出价)4100650709078789802
InMobi7681903010231960328
InMobi(出价)6325663098072678541
IronSource6925240245545091930
ironSource Ads(出价)1643326773739866623
Leadbolt2899150749497968595
LG U+AD18298738678491729107
LINE 广告联盟3025503711505004547
maio7505118203095108657
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
Mintegral1357746574408896200
Mintegral(出价)6250601289653372374
MobFox8079529624516381459
MobFox(出价)3086513548163922365
MoPub(已弃用10872986198578383917
myTarget8450873672465271579
Nend9383070032774777750
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
Tapjoy7295217276740746030
Tapjoy(出价)4692500501762622178
Tencent GDT7007906637038700218
TripleLift(出价)8332676245392738510
Unity 广告4970775877303683148
Unity Ads(出价)7069338991535737586
Verizon Media7360851262951344112
Verve Group(出价)5013176581647059185
弗蓬1940957084538325905
Liftoff Monetize*

* 在 2023 年 1 月 30 日之前,该广告联盟称为“Vungle”。

1953547073528090325
Liftoff Monetize(出价)*

* 在 2023 年 1 月 30 日之前,该广告联盟称为“Vungle(出价)”。

4692500501762622185
Yieldmo(出价)4193081836471107579
YieldOne(出价)3154533971590234104
Zucks5506531810221735863

مكافأة المستخدم

من المهم تحقيق التوازن بين تجربة المستخدم ومكافأة التحقق من الصحة عند تحديد وقت مكافأة المستخدم. قد تواجه عمليات معاودة الاتصال من جانب الخادم تأخيرات قبل الوصول إلى الأنظمة الخارجية. وبالتالي، فإنّ أفضل الممارسات الموصى بها هي استخدام معاودة الاتصال من جهة العميل لمكافأة المستخدم على الفور مع إجراء التحقُّق من جميع المكافآت عند استلام استدعاءات من جهة الخادم. يوفر هذا النهج تجربة مستخدم جيدة مع ضمان صحة المكافآت الممنوحة.

ومع ذلك، بالنسبة إلى الطلبات التي تكون فيها صلاحية المكافآت أمرًا بالغ الأهمية (على سبيل المثال، تؤثر المكافأة على اقتصاد تطبيقك داخل اللعبة) وحالات التأخير في منح المكافآت مقبولة، فقد يكون انتظار معاودة الاتصال التي تم التحقق منها من جهة الخادم هو النهج الأفضل.

البيانات المخصّصة

إنّ التطبيقات التي تتطلّب بيانات إضافية في عمليات معاودة الاتصال لإثبات الملكية من جهة الخادم يجب أن تستخدم ميزة البيانات المخصّصة للإعلانات التي تضم مكافأة. يتمّ تمرير أيّ قيمة سلسلة يتمّ ضبطها على عنصر إعلان بمكافأة إلى معلَمة طلب البحث custom_data الخاصة باستدعاء SSV. إذا لم يتم ضبط أي قيمة لبيانات مخصّصة، لن تكون قيمة مَعلمة طلب البحث custom_data متوفّرة في استدعاء SSV.

يوضح نموذج الرمز البرمجي التالي كيفية ضبط خيارات SSV بعد تحميل الإعلان بمكافأة.

Java

RewardedAd.load(MainActivity.this, "ca-app-pub-3940256099942544/5354046379",
    new AdRequest.Builder().build(),  new RewardedAdLoadCallback() {
  @Override
  public void onAdLoaded(RewardedAd ad) {
    Log.d(TAG, "Ad was loaded.");
    rewardedAd = ad;
    ServerSideVerificationOptions options = new ServerSideVerificationOptions
        .Builder()
        .setCustomData("SAMPLE_CUSTOM_DATA_STRING")
        .build();
    rewardedAd.setServerSideVerificationOptions(options);
  }
  @Override
  public void onAdFailedToLoad(LoadAdError loadAdError) {
    Log.d(TAG, loadAdError.toString());
    rewardedAd = null;
  }
});

Kotlin

RewardedAd.load(this, "ca-app-pub-3940256099942544/5354046379",
    AdRequest.Builder().build(), object : RewardedAdLoadCallback() {
  override fun onAdLoaded(ad: RewardedAd) {
    Log.d(TAG, "Ad was loaded.")
    rewardedInterstitialAd = ad
    val options = ServerSideVerificationOptions.Builder()
        .setCustomData("SAMPLE_CUSTOM_DATA_STRING")
        .build()
    rewardedAd.setServerSideVerificationOptions(options)
  }

  override fun onAdFailedToLoad(adError: LoadAdError) {
    Log.d(TAG, adError?.toString())
    rewardedAd = null
  }
})

إذا كنت تريد ضبط سلسلة المكافأة المخصّصة، عليك إجراء ذلك قبل عرض الإعلان.

إثبات الملكية اليدوي لميزة SSV التي تضم مكافأة

في ما يلي توضيح للخطوات التي تنفّذها الفئة RewardedAdsVerifier لإثبات ملكية الحساب الذي يتضمّن مكافأة SSV. على الرغم من أنّ مقتطفات الرمز المضمّنة متوفّرة بلغة Java وتستفيد من مكتبة Tink التابعة لجهة خارجية في Tink، يمكنك تنفيذ هذه الخطوات باللغة التي تختارها، وذلك باستخدام أي مكتبة تابعة لجهة خارجية توفّر ECDSA.

استرجاع المفاتيح العامة

للتحقّق من معاودة الاتصال بمكافأة من خلال ميزة "التحقُّق بخطوتين"، تحتاج إلى مفتاح عام توفّره 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 إلى المفاتيح العامة، والتي يتم تغليفها ككائنات ECPublicKey من مكتبة Tink.

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. يعرض المقتطف أدناه مثالاً لمعاودة الاتصال من خلال ميزة "التحقُّق بخطوتين" التي تضم مكافأة مع تمييز المحتوى المطلوب التحقّق منه.

https://www.myserver.com/path?ad_network=54...55&ad_unit=12345678&reward_amount=10&reward_item=coins
&timestamp=150777823&transaction_id=12...DEF&user_id=1234567&signature=ME...Z1c&key_id=1268887

يوضح الرمز أدناه كيفية تحليل المحتوى المطلوب التحقق منه من عنوان URL لمعاودة الاتصال كمصفوفة 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 من عنوان URL لمعاودة الاتصال

باستخدام القيمة queryString من الخطوة السابقة، حلّل مَعلمتَي طلب البحث signature وkey_id من عنوان URL لمعاودة الاتصال كما هو موضّح أدناه:

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()));

إجراء عملية إثبات الملكية

الخطوة الأخيرة هي التحقّق من محتوى عنوان URL لمعاودة الاتصال باستخدام المفتاح العام المناسب. خذ الربط الذي يعرضه إجراء parsePublicKeysJson واستخدِم المَعلمة key_id من عنوان URL لمعاودة الاتصال للحصول على المفتاح العام من عملية الربط هذه. ثم تحقق من التوقيع باستخدام هذا المفتاح العام. يتم توضيح هذه الخطوات أدناه في طريقة 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);
  }
}

إذا تم تنفيذ الطريقة بدون طرح استثناء، يكون قد تم التحقق من عنوان URL لمعاودة الاتصال بنجاح.

الأسئلة الشائعة

هل يمكنني إجراء تخزين مؤقت للمفتاح العام الذي قدمه خادم إدارة مفاتيح AdMob؟
ننصحك بتخزين المفتاح العام الذي يوفره خادم إدارة مفاتيح التشفير في AdMob مؤقتًا لتقليل عدد العمليات المطلوبة للتحقّق من صحة عمليات استدعاء SSV. ومع ذلك، تجدر الإشارة إلى أنّه يتم تدوير المفاتيح العامة بانتظام ويجب عدم تخزينها مؤقتًا لمدة تزيد عن 24 ساعة.
ما هو معدّل استبدال المفاتيح العامة التي يوفّرها خادم إدارة مفاتيح التشفير في AdMob؟
يتم عرض المفاتيح العامة التي يوفّرها خادم إدارة مفاتيح التشفير في AdMob بالتناوب وفقًا لجدول زمني متغيّر. لضمان استمرار عمل ميزة التحقّق من استدعاءات SSV على النحو المطلوب، يجب عدم تخزين المفاتيح العامة مؤقتًا لأكثر من 24 ساعة.
ماذا يحدث إذا تعذّر الوصول إلى الخادم؟
تتوقّع Google تلقّي رمز استجابة بحالة نجاح HTTP 200 OK من عمليات معاودة الاتصال من SSV. إذا تعذَّر الوصول إلى الخادم أو لم يوفِّر الاستجابة المتوقَّعة، ستعيد Google محاولة إرسال طلبات استرداد SSV حتى خمس مرات على فترات زمنية تبلغ ثانية واحدة.
كيف يمكنني التحقّق من أن عمليات معاودة الاتصال بالتحقُّق بخطوتين (SSV) واردة من Google؟
استخدِم بحث نظام أسماء النطاقات العكسي للتحقّق من أنّ طلبات استدعاء التحقّق من الخدمة (SSV) تنشأ من Google.