إعداد ميزة "عنوان IP من الناشر"

يمكن أن تخفي الخوادم الوكيلة لخصوصية المتصفّح وإعدادات حماية عنوان IP عناوين IP للعملاء في طلبات عرض الإعلانات التابعة لجهات خارجية إلى "إدارة إعلانات Google". يمكن أن تؤدي عناوين IP المقنَّعة إلى تقليل دقة الاستهداف الجغرافي وصلة الإعلان بنية المستخدم. بما أنّ خادم الويب أو شبكة توصيل المحتوى (CDN) يتفاعلان مع المستخدم في سياق الطرف الأول، يتلقّى خادمك عنوان IP الخاص بالمستخدم بدون قناع.

تتيح لك ميزة "عنوان IP من الناشر" تسجيل عنوان IPv4 أو IPv6 غير المحجوب للمستخدم على خادمك، وتشفير العنوان باستخدام "تشفير المفتاح العام المختلط" (HPKE)، وإرسال الحمولة المشفّرة إلى "علامة ناشر Google" (GPT) على العميل.

تتناول هذه الصفحة كيفية دمج ميزة "عنوان IP من الناشر" مع GPT.

تنفيذ سير العمل من جهة الخادم

لتجنُّب الكشف عن عنوان IP الخاص بالمستخدم كنص عادي للنصوص البرمجية التابعة لجهات خارجية في المتصفّح، اتّبِع الخطوات التالية على خادم الويب أو مشغّل Edge في شبكة توصيل المحتوى (CDN):

  • التقاط جميع عناوين IP
  • استرجِع المفتاح العام.
  • تنفيذ تشفير HPKE

تسجيل عنوان IP للعميل

استخرِج عنوان IPv4 أو IPv6 غير المقنَّع للعميل من طلب الصفحة الوارد على خادم الويب أو شبكة توصيل المحتوى (CDN):

  • الاتصالات المباشرة: قراءة عنوان المقبس البعيد لاتصال HTTP الوارد
  • خلف شبكة توصيل محتوى (CDN) أو جهاز موازنة الحمل أو خادم وكيل عكسي: اقرأ عنوان IP الخاص بالعميل من الخادم الوكيل الموثوق به، مثل عنوان X-Forwarded-For أو عنوان IP الخاص بالعميل في شبكة توصيل المحتوى (CDN). يجب الوثوق فقط في عناوين إعادة التوجيه التي تضيفها أو تلغيها البنية الأساسية للخادم الوكيل. عندما يحتوي العنوان X-Forwarded-For على عناوين متعددة مفصولة بفواصل، اختَر عنوان IP الخاص بالعميل الذي أضافه الخادم الوكيل الموثوق به.

تتيح "إدارة إعلانات Google" العناوين التالية:

  • ‫IPv4، على سبيل المثال، 203.0.113.42
  • الإصدار السادس من بروتوكول الإنترنت (IPv6)، على سبيل المثال، 2001:db8::8a2e:370:7334

يجب تمرير سلسلة عنوان IP فقط، بدون أرقام المنافذ أو الأقواس أو بادئات CIDR أو المسافات البيضاء المحيطة.

جلب المفتاح العام وتخزينه مؤقتًا

تنشر Google مفتاح تشفير HPKE النشط كمجموعة مفاتيح ويب JSON (JWKS) وفقًا لمعيار RFC 8037 في نقطة النهاية التالية:

https://www.gstatic.com/real-ip/ip_enc_key.json

يؤدي إرسال طلب GET إلى نقطة النهاية هذه إلى عرض عنصر JSON، وتحتوي مصفوفة keys الخاصة به على عنصر مفتاح نشط واحد. استخدِم المفتاح في السمة x لهذا العنصر لتشفير عنوان IP للعميل.

يجلب المثال التالي المفتاح العام ويستخرج السمة x في حال نجاح العملية، ويُظهر استثناءً في حال تعذّر تنفيذها:

جافا

private static final String KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json";

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * <p>In production, cache the returned public key in memory and refresh it at least once every 24
 * hours rather than fetching it on every incoming request.
 *
 * @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
 */
public static String fetchPublicKey() throws IOException, InterruptedException {
  return fetchPublicKey(KEY_ENDPOINT_URL);
}

public static String fetchPublicKey(String endpointUrl) throws IOException, InterruptedException {
  HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
  HttpRequest request =
      HttpRequest.newBuilder()
          .uri(URI.create(endpointUrl))
          .timeout(Duration.ofSeconds(5))
          .GET()
          .build();
  HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
  if (response.statusCode() < 200 || response.statusCode() >= 300) {
    throw new IOException("Failed to fetch public key, HTTP status: " + response.statusCode());
  }
  JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
  JsonArray keys = json.getAsJsonArray("keys");
  if (keys == null || keys.isEmpty()) {
    throw new IOException("No keys found in response from " + endpointUrl);
  }
  JsonObject keyObj = keys.get(0).getAsJsonObject();
  if (!keyObj.has("x")) {
    throw new IOException("Key payload missing 'x' property: " + keyObj);
  }
  return keyObj.get("x").getAsString();
}

Python

KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json"


def fetch_public_key(url: str = KEY_ENDPOINT_URL) -> str:
  """Fetches the active X25519 public key from Google's key endpoint.

  In production, cache the returned public key in memory and refresh it at
  least once every 24 hours rather than fetching it on every incoming request.

  Args:
    url: URL of the public key endpoint.

  Returns:
    The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
  """
  with urllib.request.urlopen(url, timeout=5) as response:
    data = json.loads(response.read().decode("utf-8"))
  keys = data.get("keys", [])
  if not keys:
    raise ValueError(f"No keys found in response from {url}")
  key_payload = keys[0].get("x")
  if not key_payload:
    raise ValueError(f"Key payload missing 'x' property: {keys[0]}")
  return key_payload


JavaScript

const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * In production, cache the returned public key in memory and refresh it at
 * least once every 24 hours rather than fetching it on every incoming request.
 *
 * @param {string=} url URL of the public key endpoint.
 * @return {!Promise<string>} The unpadded Base64URL-encoded 32-byte X25519
 *     public key ('x').
 */
async function fetchPublicKey(url = KEY_ENDPOINT_URL) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch public key, HTTP status: ${response.status}`,
    );
  }
  const data = await response.json();
  if (!data.keys || data.keys.length === 0) {
    throw new Error(`No keys found in response from ${url}`);
  }
  const keyPayload = data.keys[0].x;
  if (!keyPayload) {
    throw new Error(
      `Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
    );
  }
  return keyPayload;
}

TypeScript

const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';

interface JwkKey {
  kty?: string;
  crv?: string;
  x?: string;
  kid?: string;
}

interface KeyEndpointResponse {
  keys?: JwkKey[];
}

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * In production, cache the returned public key in memory and refresh it at
 * least once every 24 hours rather than fetching it on every incoming request.
 *
 * @param url URL of the public key endpoint.
 * @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
 */
export async function fetchPublicKey(
  url: string = KEY_ENDPOINT_URL,
): Promise<string> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch public key, HTTP status: ${response.status}`,
    );
  }
  const data = (await response.json()) as KeyEndpointResponse;
  if (!data.keys || data.keys.length === 0) {
    throw new Error(`No keys found in response from ${url}`);
  }
  const keyPayload = data.keys[0].x;
  if (!keyPayload) {
    throw new Error(
      `Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
    );
  }
  return keyPayload;
}

إذا نجح الطلب، ستتضمّن الاستجابة مصفوفة keys التي تحتوي على عنصر مفتاح مع السمة x:

{
  "keys": [
    {
      "kty": "OKP",
      "crv": "X25519",
      "x": "7bws5TIHOUrcJNofEubW3DPW8zR9EtjFNLm_Grv98TA",
      "kid": "EZMLFw"
    }
  ]
}

يوضّح الجدول التالي سمات عنصر المفتاح في مصفوفة keys:

الموقع الوصف
crv معرّف المنحنى الإهليلجي يجب دائمًا X25519.
kid معرّف مفتاح بترميز base64url غير المضاف، والذي يتغيّر عندما تُجري Google عملية تدوير للمفتاح.
kty نوع المفتاح دائمًا OKP (مفتاحا التشفير الثماني).
x مفتاح X25519 العام الأولي بحجم 32 بايت والمشفّر باستخدام base64url بدون إضافة، ويُستخدم للتشفير.

خزِّن المفتاح العام الذي تم جلبه في الذاكرة على الخادم وأعِد تحميل المفتاح مرة واحدة على الأقل كل 24 ساعة، أو قبل ذلك وفقًا لعنوان استجابة HTTP Cache-Control. لا تجلب المفتاح العام مع كل طلب صفحة وارد. تحتفظ Google بالمفاتيح الخاصة التي تم تدويرها. تستمر عمليات فك تشفير الحمولات المشفّرة باستخدام مفتاح عام تم تخزينه مؤقتًا في آخر 24 ساعة بدون انقطاع أثناء تدوير المفتاح.

تشفير عنوان IP

شفِّر تمثيل بايت UTF-8 لسلسلة عنوان IP الخاص بالعميل باستخدام HPKE (RFC 9180) والمفتاح العام X25519 الذي تم فك ترميزه بحجم 32 بايت، x، الذي تم استرداده في جلب المفتاح العام وتخزينه مؤقتًا.

يشفر المثال التالي سلسلة عنوان IP:

جافا

يستخدم هذا المثال Tink (com.google.crypto.tink:tink 1.16.0 أو إصدار أحدث):

static {
  try {
    HybridConfig.register();
  } catch (GeneralSecurityException e) {
    throw new ExceptionInInitializerError(e);
  }
}

/**
 * Encrypts an IP address using HPKE (DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-256-GCM).
 *
 * @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param ipStr Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").
 * @return Unpadded Base64URL-encoded payload (enc || ciphertext).
 */
@AccessesPartialKey
public static String encryptIp(String pubXB64Url, String ipStr) throws GeneralSecurityException {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  byte[] pubBytes = Base64.getUrlDecoder().decode(pubXB64Url);
  if (pubBytes.length != 32) {
    throw new IllegalArgumentException(
        "Public key must be exactly 32 bytes, got " + pubBytes.length);
  }

  // 2. Configure HPKE parameters (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM, NO_PREFIX).
  HpkeParameters parameters =
      HpkeParameters.builder()
          .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
          .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
          .setAeadId(HpkeParameters.AeadId.AES_256_GCM)
          .setVariant(HpkeParameters.Variant.NO_PREFIX)
          .build();

  HpkePublicKey publicKey =
      HpkePublicKey.create(parameters, Bytes.copyFrom(pubBytes), /* idRequirement= */ null);

  // 3. Encrypt the UTF-8 IP address bytes. With Variant.NO_PREFIX, Tink
  //    outputs raw (enc || ciphertext_with_tag).
  KeysetHandle handle =
      KeysetHandle.newBuilder()
          .addEntry(KeysetHandle.importKey(publicKey).withRandomId().makePrimary())
          .build();

  HybridEncrypt encryptor = handle.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class);
  byte[] payload =
      encryptor.encrypt(ipStr.getBytes(StandardCharsets.UTF_8), /* contextInfo= */ new byte[0]);

  // 4. Return unpadded Base64URL-encoded payload.
  return Base64.getUrlEncoder().withoutPadding().encodeToString(payload);
}

Python

يستخدم هذا المثال السمة pyhpke:

def encrypt_ip(pub_x_b64url: str, ip_str: str) -> str:
  """Encrypts an IP address using HPKE (RFC 9180).

  Args:
    pub_x_b64url: Base64URL-encoded 32-byte X25519 public key ('x').
    ip_str: Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").

  Returns:
    Unpadded Base64URL-encoded payload (enc || ciphertext).
  """
  # 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  pub_bytes = base64.urlsafe_b64decode(
      pub_x_b64url + "=" * (-len(pub_x_b64url) % 4)
  )
  if len(pub_bytes) != 32:
    raise ValueError(
        f"Public key must be exactly 32 bytes, got {len(pub_bytes)}"
    )

  # 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  #    AES256_GCM).
  suite = CipherSuite.new(
      KEMId.DHKEM_X25519_HKDF_SHA256, KDFId.HKDF_SHA256, AEADId.AES256_GCM
  )
  public_key = suite.kem.deserialize_public_key(pub_bytes)

  # 3. Create a fresh sender context per encryption (you may cache 'suite' and
  #    'public_key', but never reuse 'sender' across multiple seal() calls) and
  #    package (enc || ciphertext).
  enc, sender = suite.create_sender_context(public_key)
  ciphertext = sender.seal(ip_str.encode("utf-8"))

  # 4. Return unpadded Base64URL-encoded payload.
  return base64.urlsafe_b64encode(enc + ciphertext).decode("utf-8").rstrip("=")


JavaScript

يستخدم هذا المثال السمة @hpke/core:

/**
 * Encrypts an IP address using HPKE (RFC 9180).
 *
 * @param {string} pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param {string} ipStr Plaintext IPv4 or IPv6 address string (for example,
 *     "203.0.113.42").
 * @return {!Promise<string>} Unpadded Base64URL-encoded payload
 *     (enc || ciphertext).
 */
async function encryptIp(pubXB64Url, ipStr) {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  const pubBytes = Buffer.from(pubXB64Url, 'base64url');
  if (pubBytes.length !== 32) {
    throw new Error(
      `Public key must be exactly 32 bytes, got ${pubBytes.length}`,
    );
  }

  // 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM).
  const suite = new CipherSuite({
    kem: new DhkemX25519HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Aes256Gcm(),
  });
  const rawPubKey = new Uint8Array(pubBytes).buffer;
  const publicKey = await suite.kem.importKey('raw', rawPubKey, true);

  // 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
  const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
  const {enc, ct} = await suite.seal(
    {recipientPublicKey: publicKey},
    plaintext,
  );

  // 4. Return unpadded Base64URL-encoded payload.
  return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
    'base64url',
  );
}

TypeScript

يستخدم هذا المثال السمة @hpke/core:

/**
 * Encrypts an IP address using HPKE (RFC 9180).
 *
 * @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param ipStr Plaintext IPv4 or IPv6 address string (for example,
 *     "203.0.113.42").
 * @return Unpadded Base64URL-encoded payload (enc || ciphertext).
 */
export async function encryptIp(
  pubXB64Url: string,
  ipStr: string,
): Promise<string> {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  const pubBytes = Buffer.from(pubXB64Url, 'base64url');
  if (pubBytes.length !== 32) {
    throw new Error(
      `Public key must be exactly 32 bytes, got ${pubBytes.length}`,
    );
  }

  // 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM).
  const suite = new CipherSuite({
    kem: new DhkemX25519HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Aes256Gcm(),
  });
  const rawPubKey = new Uint8Array(pubBytes).buffer;
  const publicKey = await suite.kem.importKey('raw', rawPubKey, true);

  // 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
  const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
  const {enc, ct} = await suite.seal(
    {recipientPublicKey: publicKey},
    plaintext,
  );

  // 4. Return unpadded Base64URL-encoded payload.
  return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
    'base64url',
  );
}

اضبط مكتبة HPKE باستخدام مجموعة الرموز والمَعلمات التالية:

المَعلمة القيمة
آلية تغليف المفاتيح (KEM) DHKEM(X25519, HKDF-SHA256) ‏(0x0020)
دالة اشتقاق المفاتيح (KDF) HKDF-SHA256 ‏(0x0001)
التشفير المصادَق عليه مع البيانات المرتبطة (AEAD) AES-256-GCM ‏(0x0002)
الوضع الأساس (0x00)
معلومات التطبيق (info) فارغ ("")
البيانات المرتبطة (aad) فارغ ("")

يجب تنسيق الناتج المشفَّر على أنّه دمج أولي للمفتاح العام المؤقت المغلَّف بحجم 32 بايت، enc، والنص المشفَّر AEAD، بما في ذلك علامة مصادقة GCM بحجم 16 بايت، بدون بادئة Tink أو بادئة عنوان مخصّصة. ترميز النتيجة كسلسلة base64url غير مبطّنة

يجب تشفير كل حمولة كرسالة HPKE مستقلة أحادية اللقطة باستخدام زوج مفاتيح مؤقت جديد. إذا كانت مكتبة HPKE تستخدم سياق مُرسِل يتضمّن معلومات عن الحالة، أنشِئ سياقًا جديدًا لكل عملية تشفير بدلاً من إعادة استخدامه في رسائل متعددة. يجب تشفير عنوان IP للعميل لكل طلب صفحة، أو تخزين سلسلة النص المشفّر base64url الناتجة مؤقتًا لكل عنوان IP لفترة قصيرة (TTL).

تنفيذ سير العمل من جهة العميل

بعد أن ينشئ خادمك حمولة IP المشفّرة بتنسيق base64url، يجب تمريرها إلى GPT على العميل قبل طلب الإعلانات.

تمرير عنوان IP المشفّر إلى GPT

مرِّر سلسلة عنوان IP المشفّرة إلى طريقة googletag.setConfig باستخدام السمة ipAddress في PageSettingsConfig.

يُدرج المثال التالي حمولة عنوان IP المشفَّرة التي أنشأها الخادم في طريقة googletag.setConfig قبل تفعيل الخدمات وعرض خانة إعلانية:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Publisher-provided IP example</title>
    <script src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" crossorigin="anonymous" async></script>
    <script>
      window.googletag = window.googletag || {cmd: []};

      // Inject the base64url-encoded encrypted IP generated by your server.
      const encryptedClientIp = 'ENCRYPTED_IP_PAYLOAD';

      googletag.cmd.push(function() {
        // Configure the encrypted IP before enabling services.
        googletag.setConfig({
          ipAddress: encryptedClientIp,
        });

        googletag
            .defineSlot('/6355419/Travel/Europe/France/Paris', [300, 250], 'banner-ad')
            .addService(googletag.pubads());
        googletag.enableServices();
      });
    </script>
  </head>
  <body>
    <div id="banner-ad" style="width: 300px; height: 250px;"></div>
    <script>
      googletag.cmd.push(function() {
        googletag.display('banner-ad');
      });
    </script>
  </body>
</html>

استبدِل ENCRYPTED_IP_PAYLOAD بسلسلة base64url-encoded غير المبطّنة التي ينتجها خادمك.

لمحو عنوان IP تم ضبطه مسبقًا على الصفحة، اضبط السمة ipAddress على القيمة null:

googletag.setConfig({ ipAddress: null });

التحقّق من عملية الدمج

للتأكّد من أنّ GPT ترسل حمولة عنوان IP المشفّرة وأنّ "إدارة الإعلانات" يفك تشفير الحمولة، استخدِم أدوات المطوّرين في المتصفّح و"وحدة تحكّم ناشري Google":

أدوات المطوّرين المخصصة للمتصفح

  1. افتح أدوات المطوّرين في المتصفّح.
  2. اختَر علامة التبويب الشبكة.
  3. فلتر لطلبات عرض الإعلانات على /gampad/ads
  4. تأكَّد من أنّ عنوان URL للطلب يتضمّن مَعلمة طلب بحث ips غير فارغة. بما أنّ GPT يرمّز قيمة ipAddress بشكل أكبر قبل إرسال طلب عرض الإعلان، لا تتطابق مَعلمة ips مع السلسلة المشفّرة حرفيًا.

وحدة تحكم ناشري Google

  1. لفتح وحدة التحكّم، اتّبِع الخطوات الواردة في دليل وحدة تحكّم ناشري Google.
  2. انقر على علامة التبويب طلب الصفحة.
  3. تأكَّد من عدم ظهور أي تحذيرات بشأن ipAddress. في حال حدوث مشكلة في الحمولة، تسجّل وحدة التحكّم أحد التحذيرات التالية:

    • إذا لم تكن القيمة التي تم تمريرها إلى السمة ipAddress سلسلة base64 أو base64url صالحة تتراوح بين 67 و124 حرفًا، تتجاهل GPT القيمة وتحذف المَعلمة ips من طلب عرض الإعلان وتسجّل التحذير التالي:

      Invalid value encountered when calling: googletag.setConfig: {"ipAddress":"..."}. (string) ... was found at PageSettingsConfig.ipAddress. Invalid IP address format.
      
    • إذا لم تتمكّن "مدير إعلانات Google" من فك تشفير حمولة عنوان IP المقدَّمة، أو إذا لم يكن النص العادي الذي تم فك تشفيره عنوان IPv4 أو IPv6 صالحًا، تسجّل وحدة التحكّم التحذير التالي وتعود إلى عنوان IP لطلب الشبكة:

      Decryption failed on the provided encrypted client IP (supplied via googletag.setConfig({ipAddress: ...})).