Proxy về quyền riêng tư của trình duyệt và chế độ cài đặt bảo vệ địa chỉ IP có thể che giấu địa chỉ IP máy khách trên các yêu cầu quảng cáo của bên thứ ba gửi đến Google Ad Manager. Địa chỉ IP bị che giấu có thể làm giảm độ chính xác của tính năng nhắm mục tiêu theo vị trí địa lý và mức độ phù hợp của quảng cáo. Vì máy chủ web hoặc mạng phân phối nội dung (CDN) của bạn tương tác với người dùng với tư cách là bên thứ nhất, nên máy chủ của bạn sẽ nhận được địa chỉ IP không bị che giấu của người dùng.
Tính năng địa chỉ IP do nhà xuất bản cung cấp cho phép bạn ghi lại địa chỉ IPv4 hoặc IPv6 không bị che giấu của người dùng trên máy chủ, mã hoá địa chỉ đó bằng phương thức Mã hoá khoá công khai kết hợp (HPKE) và truyền tải trọng đã mã hoá đến Thẻ nhà xuất bản của Google (GPT) trên ứng dụng.
Trang này trình bày cách tích hợp địa chỉ IP do nhà xuất bản cung cấp với GPT.
Triển khai quy trình làm việc phía máy chủ
Để tránh làm lộ địa chỉ IP dạng văn bản thuần tuý của người dùng cho các tập lệnh của bên thứ ba trong trình duyệt, hãy thực hiện các bước sau trên máy chủ web hoặc trình thực thi biên CDN:
- Thực hiện tất cả hoạt động thu thập IP.
- Tìm nạp khoá công khai.
- Thực hiện mã hoá HPKE.
Ghi lại địa chỉ IP máy khách
Trích xuất địa chỉ IPv4 hoặc IPv6 không bị che giấu của ứng dụng khách từ yêu cầu trang đến trên máy chủ web hoặc CDN của bạn:
- Kết nối trực tiếp: Đọc địa chỉ cổng từ xa của kết nối HTTP đến.
- Nằm sau CDN, trình cân bằng tải hoặc proxy ngược: Đọc tiêu đề IP của máy khách từ proxy biên đáng tin cậy, chẳng hạn như tiêu đề
X-Forwarded-Forhoặc tiêu đề IP của máy khách dành riêng cho CDN. Chỉ tin tưởng các tiêu đề chuyển tiếp mà cơ sở hạ tầng proxy của bạn thêm hoặc ghi đè. Khi tiêu đềX-Forwarded-Forchứa nhiều địa chỉ được phân tách bằng dấu phẩy, hãy chọn IP máy khách mà proxy biên đáng tin cậy đã thêm.
Ad Manager hỗ trợ các địa chỉ sau:
- IPv4, ví dụ:
203.0.113.42. - IPv6, ví dụ:
2001:db8::8a2e:370:7334.
Chỉ truyền chuỗi địa chỉ IP thuần tuý, không có số cổng, dấu ngoặc, tiền tố CIDR hoặc khoảng trắng xung quanh.
Tìm nạp và lưu khoá công khai vào bộ nhớ đệm
Google xuất bản khoá mã hoá HPKE đang hoạt động dưới dạng một RFC 8037 JSON Web Key Set (JWKS) tại điểm cuối sau:
https://www.gstatic.com/real-ip/ip_enc_key.json
Yêu cầu GET đến điểm cuối này sẽ trả về một đối tượng JSON, trong đó mảng keys chứa một đối tượng khoá đang hoạt động. Sử dụng khoá trong thuộc tính x của đối tượng đó để mã hoá địa chỉ IP máy khách.
Ví dụ sau đây tìm nạp khoá công khai và trích xuất thuộc tính x khi thành công, đồng thời gửi một ngoại lệ khi không thành công:
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;
}
Nếu thành công, phản hồi sẽ bao gồm mảng keys chứa đối tượng khoá có thuộc tính x:
{
"keys": [
{
"kty": "OKP",
"crv": "X25519",
"x": "7bws5TIHOUrcJNofEubW3DPW8zR9EtjFNLm_Grv98TA",
"kid": "EZMLFw"
}
]
}
Bảng sau đây mô tả các thuộc tính của đối tượng khoá trong mảng keys:
| Thuộc tính | Mô tả |
|---|---|
crv |
Giá trị nhận dạng đường cong elip. Luôn X25519. |
kid |
Giá trị nhận dạng khoá được mã hoá base64url không có khoảng đệm, giá trị này sẽ thay đổi khi Google xoay vòng khoá. |
kty |
Loại khoá. Luôn là OKP (Cặp khoá Octet). |
x |
Khoá công khai X25519 thô 32 byte được mã hoá base64url không đệm dùng để mã hoá. |
Lưu khoá công khai đã tìm nạp vào bộ nhớ đệm trên máy chủ và làm mới khoá ít nhất 24 giờ một lần hoặc sớm hơn theo tiêu đề phản hồi HTTP Cache-Control. Đừng tìm nạp khoá công khai cho mọi yêu cầu trang đến.
Google giữ lại các khoá riêng tư đã xoay vòng. Nội dung được mã hoá bằng khoá công khai được lưu vào bộ nhớ đệm trong 24 giờ trước đó sẽ tiếp tục giải mã mà không bị gián đoạn trong quá trình xoay vòng khoá.
Mã hoá địa chỉ IP
Mã hoá biểu diễn byte UTF-8 của chuỗi địa chỉ IP của ứng dụng bằng cách sử dụng
HPKE (RFC 9180) và khoá công khai X25519 32 byte đã giải mã, x,
được truy xuất trong phần Tìm nạp và lưu khoá công khai vào bộ nhớ đệm.
Ví dụ sau đây mã hoá một chuỗi địa chỉ IP:
Java
Ví dụ này sử dụng Tink (com.google.crypto.tink:tink 1.16.0 trở lên):
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
Ví dụ này sử dụng 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
Ví dụ này sử dụng @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
Ví dụ này sử dụng @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',
);
}
Định cấu hình thư viện HPKE bằng bộ thuật toán mật mã và tham số sau:
| Tham số | Giá trị |
|---|---|
| Cơ chế đóng gói khoá (KEM) | DHKEM(X25519, HKDF-SHA256) (0x0020) |
| Hàm dẫn xuất khoá (KDF) | HKDF-SHA256 (0x0001) |
| Mã hoá đã xác thực với dữ liệu liên kết (AEAD) | AES-256-GCM (0x0002) |
| Chế độ | Cơ số (0x00) |
Thông tin ứng dụng (info) |
Trống ("") |
Dữ liệu liên kết (aad) |
Trống ("") |
Định dạng kết quả đã mã hoá dưới dạng phép nối thô của khoá công khai tạm thời được đóng gói 32 byte, enc và bản mã AEAD, bao gồm cả thẻ xác thực GCM 16 byte, không có tiền tố tiêu đề Tink hoặc tiêu đề tuỳ chỉnh. Mã hoá kết quả dưới dạng chuỗi base64url không có khoảng đệm.
Mã hoá từng tải trọng dưới dạng một thông báo HPKE một lần độc lập bằng một cặp khoá tạm thời mới. Nếu thư viện HPKE của bạn sử dụng bối cảnh người gửi có trạng thái, hãy tạo một bối cảnh mới cho mỗi lần mã hoá thay vì sử dụng lại bối cảnh đó trên nhiều thông báo. Mã hoá địa chỉ IP máy khách cho mỗi yêu cầu trang hoặc lưu chuỗi văn bản mật mã base64url thu được vào bộ nhớ đệm cho mỗi địa chỉ IP trong một khoảng thời gian tồn tại (TTL) ngắn.
Triển khai quy trình làm việc phía máy khách
Sau khi máy chủ của bạn tạo tải trọng IP base64url đã mã hoá, hãy chuyển tải trọng đó đến GPT trên ứng dụng trước khi yêu cầu quảng cáo.
Truyền IP đã mã hoá đến GPT
Truyền chuỗi IP đã mã hoá đến phương thức googletag.setConfig bằng thuộc tính ipAddress trên PageSettingsConfig.
Ví dụ sau đây chèn tải trọng IP được mã hoá do máy chủ tạo vào phương thức googletag.setConfig trước khi bật dịch vụ và hiển thị một vị trí quảng cáo:
<!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>Thay thế ENCRYPTED_IP_PAYLOAD bằng chuỗi được mã hoá base64url không có khoảng đệm mà máy chủ của bạn tạo ra.
Để xoá địa chỉ IP đã định cấu hình trước đó trên trang, hãy đặt thuộc tính ipAddress thành giá trị null:
googletag.setConfig({ ipAddress: null });
Xác minh quá trình tích hợp
Để xác minh rằng GPT gửi tải trọng IP đã mã hoá và Ad Manager giải mã tải trọng, hãy sử dụng công cụ cho nhà phát triển của trình duyệt và Bảng điều khiển dành cho nhà xuất bản của Google:
Công cụ cho nhà phát triển trên trình duyệt
- Mở công cụ cho nhà phát triển của trình duyệt.
- Chọn thẻ Mạng.
- Lọc các yêu cầu quảng cáo đến
/gampad/ads. - Xác nhận rằng URL yêu cầu có chứa tham số truy vấn
ipskhông trống. Vì GPT mã hoá thêm giá trịipAddresstrước khi gửi yêu cầu quảng cáo, nên tham sốipskhông khớp nguyên văn với chuỗi đã mã hoá của bạn.
Bảng điều khiển dành cho nhà xuất bản của Google
- Để mở bảng điều khiển, hãy làm theo các bước trong hướng dẫn về Bảng điều khiển dành cho nhà xuất bản của Google.
- Chọn thẻ Yêu cầu trang.
Xác nhận rằng không có cảnh báo
ipAddressnào xuất hiện. Nếu có vấn đề với tải trọng, bảng điều khiển sẽ ghi lại một trong các cảnh báo sau:Nếu giá trị được truyền đến thuộc tính
ipAddresskhông phải là một chuỗi base64 hoặc base64url hợp lệ có từ 67 đến 124 ký tự, thì GPT sẽ bỏ qua giá trị đó, bỏ qua tham sốipskhỏi yêu cầu quảng cáo và ghi lại cảnh báo sau:Invalid value encountered when calling: googletag.setConfig: {"ipAddress":"..."}. (string) ... was found at PageSettingsConfig.ipAddress. Invalid IP address format.Nếu Ad Manager không thể giải mã tải trọng IP được cung cấp hoặc nếu văn bản thuần tuý được giải mã không phải là địa chỉ IPv4 hoặc IPv6 hợp lệ, thì bảng điều khiển sẽ ghi lại cảnh báo sau và quay lại địa chỉ IP của yêu cầu mạng:
Decryption failed on the provided encrypted client IP (supplied via googletag.setConfig({ipAddress: ...})).