אימות בצד השרת

קריאות חוזרות (callback) של אימות בצד השרת הן בקשות לכתובות URL, עם פרמטרים של שאילתה ש-Google מרחיבה, שנשלחות על ידי Google למערכת חיצונית כדי להודיע לה שצריך לתגמל משתמש על אינטראקציה עם מודעה מתגמלת או עם מודעת מעברון מתגמלת. קריאות חוזרות (callback) של אימות בצד השרת (SSV) של מודעות מתגמלות מספקות שכבת הגנה נוספת מפני זיוף של קריאות חוזרות (callback) בצד הלקוח כדי לתגמל משתמשים.

במדריך הזה נסביר איך לאמת קריאות חוזרות (callbacks) של אימות בצד השרת (SSV) של מודעות מתגמלות באמצעות הספרייה הקריפטוגרפית של צד שלישי Tink Java Apps, כדי לוודא שהפרמטרים של השאילתה בקריאה החוזרת הם ערכים לגיטימיים. למרות שבמדריך הזה נעשה שימוש ב-Tink, אתם יכולים להשתמש בכל ספרייה של צד שלישי שתומכת ב-ECDSA. אפשר גם לבדוק את השרת באמצעות כלי הבדיקה בממשק המשתמש של AdMob.

דרישות מוקדמות

שימוש ב-RewardedAdsVerifier מספריית Tink Java Apps

מאגר GitHub‏ Tink Java Apps כולל מחלקה מסייעת RewardedAdsVerifier שמצמצמת את כמות הקוד שנדרשת לאימות קריאה חוזרת (callback) של SSV עם תגמול. באמצעות המחלקה הזו אפשר לאמת כתובת URL לקריאה חוזרת (callback) עם הקוד הבא.

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

אם השיטה verify() מופעלת בלי להעלות חריגה, כתובת ה-URL של הקריאה החוזרת אומתה בהצלחה. בקטע תגמול המשתמש מפורטות שיטות מומלצות לגבי מתי כדאי לתגמל את המשתמשים. כדי לראות פירוט של השלבים שהמחלקה הזו מבצעת כדי לאמת קריאות חוזרות של SSV במודעות מתגמלות, אתם יכולים לקרוא את הקטע אימות ידני של SSV במודעות מתגמלות.

פרמטרים של קריאה חוזרת ל-SSV

קריאות חוזרות (callback) של אימות בצד השרת מכילות פרמטרים של שאילתה שמתארים את האינטראקציה עם המודעה המתגמלת. בהמשך מפורטים השמות, התיאורים והערכים לדוגמה של הפרמטרים. הפרמטרים נשלחים בסדר אלפביתי.

שם פרמטר תיאור ערך לדוגמה
ad_network מזהה מקור המודעות של מקור המודעות שסיפק את המודעה הזו. שמות מקורות המודעות שמתאימים לערכי המזהים מפורטים בקטע מזהים של מקורות מודעות. 1953547073528090325
ad_unit מזהה יחידת המודעות ב-AdMob ששימש לבקשת המודעה המתגמלת. 2747237135
custom_data מחרוזת נתונים בהתאמה אישית כפי שסופקה על ידי customData.

אם האפליקציה לא מספקת מחרוזת של נתונים מותאמים אישית, הערך של פרמטר השאילתה הזה לא יופיע בקריאה החוזרת ל-SSV.

SAMPLE_CUSTOM_DATA_STRING
key_id המפתח שמשמש לאימות של קריאה חוזרת (callback) לאימות בצד השרת. הערך הזה ממפה למפתח ציבורי שסופק על ידי שרת המפתחות של AdMob. 1234567890
reward_amount סכום התגמול כפי שצוין בהגדרות של יחידת המודעות. 5
reward_item פריט התגמול כפי שצוין בהגדרות של יחידת המודעות. מטבעות
signature חתימה לקריאה חוזרת (callback) של SSV שנוצרה על ידי AdMob. MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY
חותמת זמן חותמת הזמן של מועד קבלת התגמול על ידי המשתמש, כזמן Epoch באלפיות שנייה. 1507770365237823
transaction_id מזהה ייחודי בקידוד הקסדצימלי לכל אירוע הענקת תגמול שנוצר על ידי AdMob. 18fa792de1bca816048293fc71035638
user_id מזהה המשתמש כפי שסופק על ידי userId.

אם האפליקציה לא מספקת מזהה משתמש, פרמטר השאילתה הזה לא יופיע בקריאה החוזרת לאימות.

1234567

מזהים של מקורות למודעות

שמות ומזהים של מקורות מודעות

שם מקור המודעה מזהה מקור המודעות
Ad Generation (בידינג)1477265452970951479
רשת AdMob5450213213286189855
Waterfall של רשת AdMob1215381445328257950
AppLovin1063618907739174004
‫AppLovin (בידינג)1328079684332308356
Bidease (בידינג)3670825090829827805
BidMachine (בידינג)7943972370566394673
Chartboost2873236629771172317
‫Chocolate Platform (בידינג)6432849193975106527
אירוע מותאם אישית18351550913290782395
DT Exchange*

* לפני 21 בספטמבר 2022, הרשת הזו נקראה 'Fyber Marketplace'.

2179455223494392917
‫DT Exchange (בידינג)8189833498765234879
Equativ (בידינג)*

* לפני 12 בינואר 2023, הרשת הזו נקראה Smart Adserver.

5970199210771591442
‫Fluct (בידינג)8419777862490735710
i-mobile5208827440166355534
Improve Digital (בידינג)159382223051638006
Index Exchange (בידינג)4100650709078789802
InMobi7681903010231960328
‫InMobi ‏ (SDK) (בידינג)8468954295581492586
InMobi Exchange (בידינג)5264320421916134407
ironSource Ads6925240245545091930
‫ironSource Ads (בידינג)1643326773739866623
Liftoff Monetize*

* לפני 30 בינואר 2023, הרשת הזו נקראה Vungle.

1953547073528090325
‫Liftoff Monetize (בידינג)*

* לפני 30 בינואר 2023, הרשת הזו נקראה "Vungle (בידינג)".

4692500501762622185
LY Ads Network3025503711505004547
‫רשת המודעות של LY (בידינג)2615812619460460513
Magnite DV+‎ (בידינג)3993193775968767067
maio7505118203095108657
Media.net (בידינג)2127936450554446159
מודעות בית בתהליך בחירת הרשת (Mediation)6060308706800320801
Meta Audience Network*

* לפני 6 ביוני 2022, הרשת הזו נקראה Facebook Audience Network.

10568273599589928883
Meta Audience Network (בידינג)*

* לפני 6 ביוני 2022, הרשת הזו נקראה Facebook Audience Network (bidding).

11198165126854996598
Mintegral1357746574408896200
Mintegral (בידינג)6250601289653372374
‫Mobfox (בידינג)3086513548163922365
‫MobileFuse (בידינג)7303547408604090310
Moloco Ads SDK (bidding)8267622065755668722
myTarget8450873672465271579
Nativo (בידינג)3240503836211327896
‫Nexxen (בידינג)*

* לפני 1 במאי 2024, הרשת הזו נקראה UnrulyX.

2831998725945605450
OneTag Exchange (בידינג)4873891452523427499
OpenX (בידינג)4918705482605678398
Pangle4069896914521993236
‫Pangle KR SDK (בידינג)12171279046073404914
‫Pangle ROW SDK (בידינג)3525379893916449117
Pangle US SDK (בידינג)15999446638585856012
‫PubMatic (בידינג)3841544486172445473
PubMatic OpenWrap SDK7702975372504485373
‫PubMatic OpenWrap SDK (בידינג)1234567890123456789
קמפיין עם מיקומים מוזמנים7068401028668408324
‫Rise (בידינג)6816468518946650043
Sharethrough (בידינג)5247944089976324188
Smaato (בידינג)3362360112145450544
Sonobi (בידינג)3270984106996027150
TripleLift (בידינג)8332676245392738510
Unity Ads4970775877303683148
‫Unity Ads (בידינג)7069338991535737586
Verve Group (בידינג)5013176581647059185
Vpon1940957084538325905
‫Yieldmo (בידינג)4193081836471107579
YieldOne (bidding)3154533971590234104
Zucks5506531810221735863

תגמול המשתמש

כשמחליטים מתי לתגמל משתמש, חשוב לאזן בין חוויית המשתמש לבין אימות התגמול. יכול להיות שיהיו עיכובים בהעברת קריאות חוזרות (callback) בצד השרת למערכות חיצוניות. לכן, השיטה המומלצת היא להשתמש בקריאה חוזרת (callback) בצד הלקוח כדי לתגמל את המשתמש באופן מיידי, ולבצע אימות של כל התגמולים עם קבלת הקריאות החוזרות בצד השרת. הגישה הזו מספקת חוויית משתמש טובה, וגם מוודאת שהתגמולים שניתנו תקפים.

עם זאת, באפליקציות שבהן תוקף הפרס הוא קריטי (לדוגמה, אם הפרס משפיע על הכלכלה במשחק באפליקציה) ועיכובים במתן הפרסים הם מקובלים, יכול להיות שהגישה הטובה ביותר היא להמתין לקריאה החוזרת המאומתת בצד השרת.

נתונים בהתאמה אישית

באפליקציות שנדרשים בהן נתונים נוספים בקריאות חוזרות (callback) של אימות בצד השרת (SSV), צריך להשתמש בתכונה "נתונים מותאמים אישית" של מודעות מתגמלות. כל ערך מחרוזת שמוגדר באובייקט של מודעה מתגמלת מועבר לפרמטר השאילתה custom_data של הקריאה החוזרת ל-SSV. אם לא מוגדר ערך של נתונים מותאמים אישית, הערך של פרמטר השאילתה custom_data לא יופיע בקריאה החוזרת ל-SSV.

בדוגמה הבאה מוצגות הגדרות של אפשרויות SSV אחרי שהמודעה המתגמלת נטענת:

RewardedAd.load(
  adUnitId: "_adUnitId",
  request: AdRequest(),
  rewardedAdLoadCallback: RewardedAdLoadCallback(
    onAdLoaded: (ad) {
      ServerSideVerificationOptions _options =
          ServerSideVerificationOptions(
            customData: 'SAMPLE_CUSTOM_DATA_STRING',
          );
      ad.setServerSideOptions(_options);
      _rewardedAd = ad;
    },
    onAdFailedToLoad: (error) {},
  ),
);

מחליפים את SAMPLE_CUSTOM_DATA_STRING בנתונים בהתאמה אישית.

אם רוצים להגדיר את מחרוזת התגמול בהתאמה אישית, צריך לעשות זאת לפני הצגת המודעה.

אימות ידני של SSV במודעות מתגמלות

בהמשך מפורטים השלבים שמבצעת המחלקה RewardedAdsVerifier כדי לאמת SSV עם תגמול. למרות שקטעי הקוד שכלולים במאמר הזה הם ב-Java ומתבססים על ספריית הצד השלישי Tink, אתם יכולים ליישם את השלבים האלה בשפה שתבחרו, באמצעות כל ספריית צד שלישי שתומכת ב-ECDSA.

אחזור מפתחות ציבוריים

כדי לאמת קריאה חוזרת (callback) של SSV במודעה מתגמלת, צריך מפתח ציבורי ש-AdMob מספקת.

אפשר לאחזר משרת המפתחות של AdMob רשימה של מפתחות ציבוריים שישמשו לאימות הקריאות החוזרות (callbacks) של ה-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();
}

חשוב לדעת שמפתחות ציבוריים עוברים רוטציה באופן קבוע. תקבלו אימייל שיודיע לכם על רוטציה קרובה. אם אתם שומרים מפתחות ציבוריים במטמון, אתם צריכים לעדכן את המפתחות אחרי שתקבלו את האימייל הזה.

אחרי שאחזרתם את המפתחות הציבוריים, צריך לנתח אותם. ה-method‏ 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;
}

איך מקבלים תוכן לאימות

שני הפרמטרים האחרונים של השאילתה בקריאות חוזרות (callback) של SSV במודעות מתגמלות הם תמיד signature ו-key_id,, בסדר הזה. שאר הפרמטרים של השאילתה מציינים את התוכן שצריך לאמת. נניח שהגדרתם את AdMob כך שישלח קריאות חוזרות (callbacks) של תגמולים אל https://www.myserver.com/mypath. בקטע הקוד הבא מוצגת דוגמה לקריאה חוזרת (callback) של SSV במודעה מתגמלת, כשהתוכן שצריך לאמת מודגש.

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

קבלת חתימה ומזהה מפתח מכתובת 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 לקריאה חוזרת (callback) באמצעות המפתח הציבורי המתאים. לוקחים את המיפוי שהוחזר מה-method‏ parsePublicKeysJson ומשתמשים בפרמטר key_id מכתובת ה-URL של ה-callback כדי לקבל את המפתח הציבורי מהמיפוי הזה. לאחר מכן, מאמתים את החתימה באמצעות המפתח הציבורי הזה. השלבים האלה מודגמים בהמשך בשיטה 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, כדי לצמצם את מספר הפעולות שנדרשות לאימות של קריאות חוזרות (callbacks) לאימות של צפיות חוזרות. עם זאת, חשוב לזכור שהמפתחות הציבוריים מתחלפים באופן קבוע, ולכן לא מומלץ לשמור אותם במטמון למשך יותר מ-24 שעות.
באיזו תדירות מתבצעת רוטציה של המפתחות הציבוריים ששרת המפתחות של AdMob מספק?
המפתחות הציבוריים שסופקו על ידי שרת המפתחות של AdMob עוברים רוטציה בלוח זמנים משתנה. כדי לוודא שהאימות של קריאות חוזרות (callbacks) של SSV ימשיך לפעול כמו שצריך, אין לשמור מפתחות ציבוריים במטמון למשך יותר מ-24 שעות.
מה קורה אם אי אפשר להגיע לשרת שלי?
‫
Google מצפה לקוד תגובה של סטטוס הצלחה HTTP 200 OK עבור קריאות חוזרות (callbacks) של SSV. אם אי אפשר להגיע לשרת או שהוא לא מספק את התשובה הצפויה, Google תנסה שוב לשלוח קריאות חוזרות של SSV עד חמש פעמים במרווחי זמן של שנייה אחת.
איך אפשר לוודא שהקריאות החוזרות של SSV מגיעות מ-Google?
כדי לוודא שהקריאות החוזרות לאימות בצד השרת מגיעות מ-Google, צריך להשתמש בשאילתת DNS הפוכה.