Proxy privasi browser dan setelan Perlindungan IP dapat menyamarkan alamat IP klien pada panggilan iklan pihak ketiga ke Google Ad Manager. Alamat IP yang disamarkan dapat mengurangi akurasi penargetan geografis dan relevansi iklan. Karena server web atau jaringan penayangan konten (CDN) Anda berinteraksi dengan pengguna dalam konteks pihak pertama, server Anda akan menerima alamat IP pengguna yang tidak disamarkan.
Fitur IP yang diberikan penayang memungkinkan Anda mengambil alamat IPv4 atau IPv6 pengguna yang tidak disamarkan di server Anda, mengenkripsi alamat tersebut menggunakan Hybrid Public Key Encryption (HPKE), dan meneruskan payload terenkripsi ke Tag Penayang Google (GPT) di klien.
Halaman ini membahas cara mengintegrasikan IP yang diberikan penayang dengan GPT.
Menerapkan alur kerja sisi server
Agar alamat IP teks biasa pengguna tidak terekspos ke skrip pihak ketiga di browser, lakukan langkah-langkah berikut di server web atau edge worker CDN Anda:
- Melakukan semua pengambilan IP.
- Ambil kunci publik.
- Melakukan enkripsi HPKE.
Mendapatkan alamat IP klien
Ekstrak alamat IPv4 atau IPv6 klien yang tidak disamarkan dari permintaan halaman masuk di server web atau CDN Anda:
- Koneksi langsung: Membaca alamat soket jarak jauh dari koneksi HTTP yang masuk.
- Di balik CDN, load balancer, atau reverse proxy: Baca header IP klien dari proxy edge tepercaya Anda, seperti header
X-Forwarded-Foratau header IP klien khusus CDN. Hanya percayai header penerusan yang ditambahkan atau diganti oleh infrastruktur proxy Anda. Jika headerX-Forwarded-Forberisi beberapa alamat yang dipisahkan koma, pilih IP klien yang ditambahkan oleh proxy edge tepercaya.
Ad Manager mendukung alamat berikut:
- IPv4, misalnya,
203.0.113.42. - IPv6, misalnya,
2001:db8::8a2e:370:7334.
Teruskan hanya string alamat IP kosong, tanpa nomor port, tanda kurung, awalan CIDR, atau spasi kosong di sekitarnya.
Mengambil dan meng-cache kunci publik
Google memublikasikan kunci enkripsi HPKE aktif sebagai JSON Web Key Set (JWKS) RFC 8037 di endpoint berikut:
https://www.gstatic.com/real-ip/ip_enc_key.json
Permintaan GET ke endpoint ini menampilkan objek JSON, yang array keys-nya
berisi satu objek kunci aktif. Gunakan kunci di properti x objek tersebut untuk mengenkripsi alamat IP klien.
Contoh berikut mengambil kunci publik dan mengekstrak properti x jika berhasil, serta menampilkan pengecualian jika gagal:
Java
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;
}
Jika berhasil, respons akan menyertakan array keys yang berisi objek kunci
dengan properti x:
{
"keys": [
{
"kty": "OKP",
"crv": "X25519",
"x": "7bws5TIHOUrcJNofEubW3DPW8zR9EtjFNLm_Grv98TA",
"kid": "EZMLFw"
}
]
}
Tabel berikut menjelaskan properti objek kunci dalam array keys:
| Properti | Deskripsi |
|---|---|
crv |
ID kurva eliptik. Selalu X25519. |
kid |
ID kunci berenkode base64url tanpa padding, yang berubah saat Google merotasi kunci. |
kty |
Jenis kunci. Selalu OKP (Pasangan Kunci Octet). |
x |
Kunci publik X25519 mentah 32 byte berenkode base64url tanpa padding yang digunakan untuk enkripsi. |
Simpan kunci publik yang diambil dalam cache di memori di server Anda dan muat ulang kunci tersebut setidaknya sekali setiap 24 jam, atau lebih cepat sesuai dengan header respons Cache-Control HTTP. Jangan mengambil kunci publik pada setiap permintaan halaman yang masuk.
Google mempertahankan kunci pribadi yang dirotasi. Payload yang dienkripsi dengan kunci publik yang di-cache dalam 24 jam sebelumnya akan terus didekripsi tanpa gangguan selama rotasi kunci.
Mengenkripsi alamat IP
Enkripsi representasi byte UTF-8 dari string alamat IP klien menggunakan
HPKE (RFC 9180) dan kunci publik X25519 32 byte yang didekode, x,
yang diambil di Mengambil dan meng-cache kunci publik.
Contoh berikut mengenkripsi string alamat IP:
Java
Contoh ini menggunakan Tink (com.google.crypto.tink:tink 1.16.0 atau
yang lebih baru):
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
Contoh ini menggunakan 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
Contoh ini menggunakan @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
Contoh ini menggunakan @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',
);
}
Konfigurasi library HPKE Anda dengan cipher suite dan parameter berikut:
| Parameter | Nilai |
|---|---|
| Key Encapsulation Mechanism (KEM) | DHKEM(X25519, HKDF-SHA256) (0x0020) |
| Key Derivation Function (KDF) | HKDF-SHA256 (0x0001) |
| Authenticated Encryption with Associated Data (AEAD) | AES-256-GCM (0x0002) |
| Mode | Base (0x00) |
Info aplikasi (info) |
Kosong ("") |
Data terkait (aad) |
Kosong ("") |
Format output terenkripsi sebagai penyambungan mentah dari kunci publik sementara yang dienkapsulasi 32 byte, enc, dan ciphertext AEAD, termasuk tag autentikasi GCM 16 byte, tanpa Tink atau awalan header kustom. Enkode hasilnya sebagai
string base64url tanpa padding.
Enkripsi setiap payload sebagai pesan HPKE sekali pakai mandiri dengan pasangan kunci sementara yang baru. Jika library HPKE Anda menggunakan konteks pengirim stateful, buat konteks baru untuk setiap enkripsi, bukan menggunakannya kembali di beberapa pesan. Enkripsi alamat IP klien per permintaan halaman, atau simpan string ciphertext base64url yang dihasilkan per alamat IP dalam cache untuk time to live (TTL) yang singkat.
Mengimplementasikan alur kerja sisi klien
Setelah server Anda membuat payload IP base64url terenkripsi, teruskan payload ke GPT di klien sebelum meminta iklan.
Meneruskan IP terenkripsi ke GPT
Teruskan string IP terenkripsi ke metode googletag.setConfig
menggunakan properti ipAddress di
PageSettingsConfig.
Contoh berikut menyisipkan payload IP terenkripsi yang dihasilkan server
ke dalam metode googletag.setConfig sebelum mengaktifkan layanan dan menampilkan
slot iklan:
<!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>Ganti ENCRYPTED_IP_PAYLOAD dengan string berenkode base64url tanpa padding yang dihasilkan server Anda.
Untuk menghapus alamat IP yang dikonfigurasi sebelumnya di halaman, tetapkan properti ipAddress
ke nilai null:
googletag.setConfig({ ipAddress: null });
Memverifikasi integrasi
Untuk memverifikasi bahwa GPT mengirim payload IP terenkripsi dan Ad Manager mendekripsi payload, gunakan alat developer browser Anda dan Konsol Penayang Google:
Alat developer browser
- Buka alat developer browser Anda.
- Pilih tab Jaringan.
- Filter untuk permintaan iklan ke
/gampad/ads. - Pastikan URL permintaan menyertakan parameter kueri
ipsyang tidak kosong. Karena GPT mengenkode lebih lanjut nilaiipAddresssebelum mengirim panggilan iklan, parameteripstidak cocok dengan string terenkripsi Anda secara verbatim.
Konsol Penayang Google
- Untuk membuka konsol, ikuti langkah-langkah dalam panduan Konsol Penayang Google.
- Pilih tab Permintaan Halaman.
Pastikan tidak ada peringatan
ipAddressyang muncul. Jika ada masalah dengan payload, konsol akan mencatat salah satu peringatan berikut:Jika nilai yang diteruskan ke properti
ipAddressbukan string base64 atau base64url yang valid antara 67 dan 124 karakter, GPT akan mengabaikan nilai tersebut, menghilangkan parameteripsdari panggilan iklan, dan mencatat peringatan berikut:Invalid value encountered when calling: googletag.setConfig: {"ipAddress":"..."}. (string) ... was found at PageSettingsConfig.ipAddress. Invalid IP address format.Jika Ad Manager tidak dapat mendekripsi payload IP yang diberikan, atau jika teks biasa yang didekripsi bukan alamat IPv4 atau IPv6 yang valid, konsol akan mencatat peringatan berikut dan kembali ke alamat IP permintaan jaringan:
Decryption failed on the provided encrypted client IP (supplied via googletag.setConfig({ipAddress: ...})).