Skip to content
Charles Shao
Go back

IAB TCF 与 GPP 深挖:每个 AdTech 工程师都绕不开的『同意』协议

Updated:
views

2023 年 6 月,法国数据保护机构 CNIL 对 Criteo 开出 4000 万欧元 罚单,理由是它无法证明被自己重定向的用户真的同意过。CNIL 并没说 Criteo 主观恶意,而是说 Criteo 拿不出一条能用的证据链——对任意一次具体的出价,它没有任何可审计的方式去证明:背后那个用户说过”可以”。

这正是 IAB TCF 存在要解决的问题。

TCF 属于那种罕见的 AdTech 协议——它长得像一个传输格式,本质却是一份法律合同。它把 GDPR 那条抽象义务——“只有在具备合法性基础时才能处理个人数据”——编码成一串 Base64,让上百家公司在 100ms 以内经 OpenRTB 把它传来传去,每一家都独立判断:眼前这个用户,是否同意了我接下来要做的那件事。

这篇是一份在线工程师对该协议的实操走查:CMP 在页面上做什么、一条 TC String 里到底打包了什么、OpenRTB 怎么携带它、你的服务端收到后必须做什么,以及 GPP 怎么把 TCF 包进一个更大的全球合规信封。 不是一篇法律科普——但每一节都落在一个真实的 bidder、ad server 或测量供应商为了别上 CNIL 下一份新闻稿,必须落地实现的东西上。

范围说明:在 App 广告 SDK 那篇里,我把端上的同意管理与合规一笔带过、说”协议基础另文展开”——这篇就是那篇。和 RTA 里”fail-open 是一个正当的商业选择”恰好相反,TCF 里的 fail-open 几乎永远是一桩正在酝酿的合规事故,下文 §6.4 会专门讲清这个对照。

TL;DR

怎么读这篇(按角色取所需):赶时间的工程师,§3(TC String 解剖)+ §4.4 / §4.5(consent vs LI、publisher restrictions)+ §6(端到端集成)+ §8(生产坑) 是必读主干,足够你负责任地上线。§4.1–§4.3(11 个 purpose / feature 全枚举)当速查参考,第一遍可跳读、用到再回来查。§7(GPP)和 §9(可观测性) 在你公司走向全球或监管再加一部州法时再读不迟。

Table of contents

Open Table of contents

1. TCF 为什么存在:从 GDPR 到一个传输协议

2018 年 5 月 GDPR 生效时,AdTech 立刻发现自己处境尴尬。

一次广告曝光会触及几十家公司——媒体、SSP、ad exchange、DSP、DMP、CDP、测量供应商、验证供应商、CMP——其中任何一家都可能在链路上”处理个人数据”。GDPR 要求每一次处理行为都建立在一个合法性基础之上,而在广告里真正用得上的两个选项是同意(consent,Art. 6(1)(a))正当利益(legitimate interest,Art. 6(1)(f))

这立刻带来三个工程问题:

IAB 的答案就是 TCF:一份标准化的统一 vendor 注册表,加一张按 vendor ID 索引的位图,让这 30 家公司共享同一份序列化的同意状态。

整套框架立在三块基石上:

支柱是什么由谁维护
GVL(Global Vendor List)一个 JSON 文件,列出每一家 TCF 注册 vendor,各有唯一 ID,并声明它需要哪些 purpose / featureIAB Europe,按月版本化
TC String一个 base64url 编码的位域,编码这个用户授权了哪些 vendor 和 purpose用户交互后由 CMP 生成
CMP(Consent Management Platform)一个页面上的 SDK,弹出同意 UI、收集选择、生成并持久化 TC String,再通过标准 API 暴露它媒体选定的第三方——OneTrust、Sourcepoint、Didomi、Cookiebot 等——或自建

最小心智模型:CMP 是生产者,TC String 是产物,GVL 是 schema,每一个下游 vendor 都是消费者。

2. 四个角色与数据流

TCF 数据流:用户与媒体网页 / App(GDPR 数据控制者)交互;媒体加载 CMP 前端 SDK;CMP 一边把 TC String 持久化到第一方 cookie / localStorage / App 存储,一边通过 window.__tcfapi 把 TCData 暴露给页面上所有 vendor SDK;TC String 随后经 OpenRTB 搭车到服务端 vendor(DSP / SSP / 测量),后者解码它并跑 vendor 与 purpose 的双重授权检查——是则行动,否则 fail-closed 跳过 / 仅上下文 生产者 → 产物 → 消费者的流向:CMP 产出 TC String,页面通过 __tcfapi 暴露它,每个下游 vendor 各自独立解码、各自判断自己是否被授权行动。最终判断永远是 vendor AND purpose——把这个 AND 搞错,是最常见的合规失败(见 §6.3)。

四个角色:

值得点一句:媒体本身也是一个特殊的 vendor。TC String 里有一个专门的段叫 Publisher TC,装的是用户对媒体自身数据用途的同意(独立于第三方 vendor 同意)。它是服务端代码最常忘记其存在的那个段。

3. TC String:协议层的解剖

TC String 就是那个传输格式。一条真实的长这样:

CQi298AQi298AEsACCENCbFoAP_gA...AA.ILCJB_C7NbXFmybZ36...

各段用 . 拼接。哪些必需、哪些可选随版本变过;截至 2025 年 4 月的 v2.3 规范更新,规则是:

必需?内容
Core String必需元数据 + purpose 的 consent / LI + vendor 的 consent / LI + publisher restrictions
Disclosed Vendorsv2.3 起必需CMP 实际向用户披露过的那组 vendor(解决了 special-purpose-only vendor 长期以来的一处歧义)
Publisher TC可选媒体自身的 purpose 同意,含任何自定义 purpose

TC String 段结构:三个盒子用一个字面量 '.' 字符连接——Core String(必需,装元数据、purpose 的 consent/LI、vendor 的 consent/LI、publisher restrictions),然后 Disclosed Vendors(v2.3 起必需,CMP 向用户披露过的那组 vendor),然后 Publisher TC(可选,媒体自身的 purpose 同意) TC String 是各段用字面量 . 拼接而成。Core 永远在;Disclosed Vendors 在 v2.3 变成强制;Publisher TC 可选,也是服务端代码最常忘记去读的那个段。

如果你的服务端代码写于 2025 年 4 月之前,单段字符串(只有 Core)仍能解码,但形式上已不完整。现代 CMP 发的是两段或三段的字符串。

3.1 VendorConsents:bitfield 与 range 的取舍

规范里工程上最有意思的细节,是 VendorConsents(以及与之对应的 VendorLegitimateInterests)怎么编码。段里有一个位 IsRangeEncoding,在两种布局之间切换:

哪个更短,完全取决于被授权集合有多稀疏

场景更短的编码
用户点了”全部接受” → 几乎每个 vendor ID = 1Bitfield(一长串 1)
用户点了”全部拒绝” → 几乎每个 vendor ID = 0Bitfield(一长串 0)
用户只对少数几家 vendor opt-in → 高度稀疏Range——而且优势很大

CMP 把两种都算一遍、取更小的那个。对单个用户来说差别可以忽略,但 cookie 有硬性大小上限(多数浏览器是每条 cookie 4KB),而 TCF 字符串远不是唯一在抢这点预算的东西。

3.2 解码一条真实的 TC String

下面是一条真实的生产 TC String(Core + Disclosed Vendors,两段):

CQi298AQi298AEsACCENCbFoAP_gAEPgAATAK8pB_C7NbWFiybZ3aLtEcAhHV9BjbsQwAAaJA2ABTDqWoJwHw0EYFAzQlKIKGQIAqiTBIQIkCACABUCgIIAFgSBMQEyQoDBKIoAggBMBAGFYAExiGIJAEQCIAKIUNEEAmBwJocIUWEDwywAABhKRUAQxEAAAAIagCYFcBAMAC8IEdAgMKYIIjEDAEGQKAlkbpCABojQYQhEtVAkAFiTyONgHDkZDgXUhEvoYsAlxm4oCCwiQfwuzW1xZsiwV-C7RDAIV1fQA27EMAAGiQNgAUw6lqCMA9tBEgRIkBCmCBkAAKIkQSAAKAgIAARAoQCAFQEATABMEKAwSADAIIAzAUBgUABIQgiCQBEAiACiFDVBQIkMCKDCFlhAsEkAICYSgVAEERAAAAiCABmBTAABAAriBPQQCECCCIxAwBBACAIZG5QgBaB0GMIRjVSIABQkcjDAFyxAQ8F1IQDoGLGJZNqKggBAA.ILCJB_C7NbXFmybZ36LtEcAhXV9BjbsQwAAaJA2ABTDqWoJwH20EaFEzQlKYKGQIAqiTBIQIsCAiABUChIIAVgSBMQEyQoDBKIsAggDMBQGFYAExiGIJAEQCIAKIUNUFAmRwJocIWWEDwywAgJhKRUAQxEAAACIagGYFcBAMAC-IE9BgMaYIIjEDAEGQKAlkbtCAFonQYwhGtVIkAFiTyONgXLkZDwXUhEvoYsYl124qCAE

把它贴进官方解码器 iabgpp.com/#/decode,会得到大致如下结果:

字段
version2
cmpId300(Sourcepoint
cmpVersion / consentScreen / consentLanguage2 / 2 / EN
vendorListVersion155——你的服务端应当拉 GVL v155 才能正确解读这条字符串,见 §8.1
policyVersion5——v2.2 上线时是 4;随 GVL v3 一起升到 5。读它,别硬编码
isServiceSpecifictrue
publisherCountryCodeCY(塞浦路斯)
purposeConsents{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}——11 个 purpose 全部 opt-in
purposeLegitimateInterests{2, 7, 8, 9, 10, 11}——注意缺了 1、3、4、5、6:purpose 1 按定义只能走 consent;3/4/5/6 是 v2.2 砍掉 LI 选项的”画像四件套”
specialFeatureOptins{1}——opt-in 了精确地理位置,但没 opt-in 设备指纹
vendorConsents 数量1,401 家 vendor
vendorLegitimateInterests 数量1,412 家 vendor

一个教科书式的”全部接受”——1,400 家 vendor 的同意率,强烈暗示这个 CMP 的”全部接受”按钮在视觉上压过了”全部拒绝”按钮。v2.2 之后,这件事本身就是一项合规风险(见 §8)。

4. 目的、特征与合法性基础

按现行 IAB Europe TCF Policies, Appendix A,TCF v2.2/v2.3 定义了 11 个 Purpose3 个 Special Purpose3 个 Feature2 个 Special Feature。vendor 在 GVL 里声明自己需要哪些,用户通过 CMP 决定。

4.1 11 个 Purpose(取自 Appendix A 原文)

ID官方名称一句话是什么
1Store and/or access information on a deviceCookie、localStorage、设备 ID。**唯一允许的合法性基础:Consent。**这是 Appendix A 里唯一明确钉死法律依据的 purpose——它直接对应 ePrivacy Directive Art. 5(3)。
2Use limited data to select advertising非个性化(上下文)选广告,外加 frequency capping
3Create profiles for personalised advertising为广告定向建立逐用户画像
4Use profiles to select personalised advertising用画像去定向某个用户
5Create profiles to personalise content同 3,用于编辑内容
6Use profiles to select personalised content同 4,用于编辑内容
7Measure advertising performance归因、转化、可见性
8Measure content performance内容侧的同上
9Understand audiences through statistics or combinations of data from different sources受众测量、面板派生的人口统计
10Develop and improve services产品改进、ML 模型训练
11Use limited data to select contentv2.2 新增——purpose 2 在内容侧的上下文对应物

4.2 3 个 Special Purpose

special purpose 只能在**正当利益(LI)**下处理——用户无法拒绝。理由是:没有它们,平台要么坏掉,要么违反其他法律。

ID官方名称现实例子
1Ensure security, prevent and detect fraud, and fix errorsIVT 检测、反 bot、错误恢复
2Deliver and present advertising and content把广告字节从服务器搬到用户——用到 IP、UA、屏幕尺寸这些你压根跳不过的东西
3Save and communicate privacy choices元目的:持久化并传递 TCF 状态本身。CMP 和 vendor 用它来读写 TC String

4.3 Feature 与 Special Feature

Feature(3 个)——vendor 声明它们是追求某个 purpose 时所用的手段,没有单独的用户勾选项,但要披露:

ID官方名称
1Match and combine data from other data sources
2Link different devices
3Identify devices based on information transmitted automatically(经 IP / UA / TCP 信号的被动指纹)

Special Feature(2 个)——需要用户显式 opt-in,而不只是泛泛的同意:

ID官方名称
1Use precise geolocation data(精度低于 500m,或经纬度小数点后多于 2 位)
2Actively scan device characteristics for identification(主动指纹——字体、插件、屏幕分辨率)

Feature 3 与 Special Feature 2 的区别很关键。前者是”我们接收并使用自动到达的设备信息”——只需披露。后者是”我们主动探测设备的可识别特征”——需显式 opt-in。如果你的产品做任何形式的指纹识别,你必须清楚自己站在这条线的哪一侧,因为多数 CMP UI 把 special feature 折进了二级菜单,opt-in 率惨不忍睹。在欧盟把商业模式建在 Special Feature 2 上,基本是行不通的。

每个 purpose 最多有两种可能的合法性基础(v2.2 关掉了好几个):

对单个 purpose 的正确服务端判断:

对我需要的每个 purpose P:
  在 GVL 里查这个 vendor 为 P 声明的法律依据。
    声明 consent     → 检查 PurposesConsent[P]        == 1
    声明 LI          → 检查 PurposesLITransparency[P]  == 1
    声明 flexible    → 看默认值,再叠加任何
                       publisher restriction 的覆盖
    未声明           → fail

**这是 TCF 里被实现得最离谱的一步。**常见的失败模式:

规则是:信 vendor 的 GVL 声明,别信你的直觉。

4.5 Publisher restrictions:覆盖机制

媒体有权在逐 purpose 层面覆盖一个 vendor 声明的法律依据。三种 restriction 类型:

类型含义
0vendor 完全不得使用此 purpose(硬禁)
1此 purpose 上 vendor 必须依赖 consent,哪怕它的 GVL 声明了 LI
2此 purpose 上 vendor 必须依赖 LI,哪怕它的 GVL 声明了 consent

restriction 住在 Publisher TC 段里(某些情况下在 Core 里)。**服务端决策顺序很重要:先应用 publisher restrictions,再检查那个对的位。**跳过这一步,是生产代码即便在开发者自以为懂 TCF 时仍会脱离合规的最常见原因。

5. Scope:如今只剩一种合法模式

历史上规范通过 Core String 里的 IsServiceSpecific 位定义了两种 scope 模式:

Scope含义今日状态
Service-SpecificIsServiceSpecific = 1TC String 只对当前媒体 / 媒体组有效,存在媒体自己的域名下唯一合法模式
GlobalIsServiceSpecific = 0consensu.org 共享域名在所有媒体间共享 TC String,配一套 Out-of-Band(OOB)披露机制2021 年 9 月 1 日起废弃

现行技术规范说得毫不含糊:“This field must always have the value of 1. When a Vendor encounters a TC String with IsServiceSpecific = 0 then it is considered invalid.”

换句话说:IsServiceSpecific != 1 不是一个向后兼容问题,而是一条无效字符串。fail-closed,丢弃它。

global scope 为什么死了:它依赖一个 consensu.org 的第三方 cookie,而 Safari ITP 加上各浏览器更大范围的第三方 cookie 弃用,早在 IAB 正式宣判它之前就让它失效了。

一句话:服务端只认 IsServiceSpecific = 1。见到 0,当无效字符串处理、fail-closed——别把它当成需要兼容的旧格式。

6. 端到端集成:工程师的实操走查

6.1 客户端:__tcfapi 这套 CMP API

IAB 标准化了 CMP 的 JavaScript 接口面。每个 TCF v2 合规的 CMP 都必须暴露 window.__tcfapi

window.__tcfapi('getTCData', 2, function(tcData, success) {
  if (!success || !tcData) return;
  if (tcData.cmpStatus !== 'loaded') return;
  if (tcData.eventStatus !== 'tcloaded' &&
      tcData.eventStatus !== 'useractioncomplete') return;

  // tcData.tcString 就是我们要发往后端的那个传输格式。
  // tcData.gdprApplies 告诉我们 GDPR 是否适用。
  loadAdSDK(tcData.tcString, tcData.gdprApplies);
});

addEventListener 订阅同意变化——比如用户回到 CMP 二级菜单,把”全部接受”改成”全部拒绝”。收到 useractioncomplete 时,广告 SDK 应当停止任何刚刚失去合法性基础的数据采集:

window.__tcfapi('addEventListener', 2, function(tcData, success) {
  if (!success) return;
  if (tcData.eventStatus === 'useractioncomplete') {
    revalidatePermissions(tcData);
  }
});

TCData 对象上的核心字段:

字段含义
tcString完整 TC String
cmpIdcmpVersionCMP 标识
gdprAppliestrue / false / undefined
eventStatustcloaded | cmpuishown | useractioncomplete
cmpStatusstub | loading | loaded | error
listenerIdremoveEventListener
purpose.consents[1..11]逐 purpose 的 consent
purpose.legitimateInterests[1..11]逐 purpose 的 LI
vendor.consents[<vendorId>]你的 vendor ID 是否有 consent
vendor.legitimateInterests[<vendorId>]你的 vendor ID 是否有 LI
specialFeatureOptins[1..2]special feature 的 opt-in
publisher.restrictionspublisher 覆盖

CMP stub 陷阱:在完整 CMP 加载之前,__tcfapi 就已经存在——每个 CMP 都被要求在 <head> 元素之前注入一个 stub。在 stub 状态下调 getTCData 会返回 cmpStatus === 'stub' 和一个毫无意义的 tcString永远别拿一个 stub 状态的响应去做数据采集决策。

6.2 OpenRTB:四个字段的世界

在 CMP 和后端之间,TC String 搭车在 OpenRTB 的扩展字段上。标准位置:

OpenRTB 2.5(扩展字段):

{
  "id": "bid-request-id",
  "imp": [...],
  "user": {
    "ext": {
      "consent": "CPubcgAPubcgABABBENBgWAAAAAAAAAAAAAAAAEXkAAA.YAAAAAAAAAAA"
    }
  },
  "regs": {
    "ext": {
      "gdpr": 1
    }
  }
}

OpenRTB 2.6 把它们提升为一等字段,并加入了 GPP:

{
  "user": {
    "consent": "CPubcgAPubcgABABBENBgWAAAAAAAAAAAAAAAAEXkAAA.YAAAAAAAAAAA"
  },
  "regs": {
    "gdpr": 1,
    "gpp": "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
    "gpp_sid": [2]
  }
}

生产里你两套都得处理——SSP 异步升级,你会在同一个小时里同时见到 user.consentuser.ext.consent。一段防御式的提取:

function extractTcString(bidRequest: any): string | undefined {
  return (
    bidRequest?.user?.consent ??
    bidRequest?.user?.ext?.consent
  );
}

function extractGdprApplies(bidRequest: any): boolean {
  const v =
    bidRequest?.regs?.gdpr ??
    bidRequest?.regs?.ext?.gdpr;
  return v === 1 || v === '1';
}

6.3 服务端决策流程

bidder 收到请求后的决策流水线,大致是 gdpr → tc_string →(可选 CMP)→ publisher restrictions → vendor → purpose →(可选 special feature)

服务端 TCF 决策流水线的流程图:第 1 步 gdprApplies == 1?——若为 0 / 缺失,跳过 TCF、套用其他辖区规则;若适用,第 2 步 TC String 在不在、能否解码?——若缺失或解码失败,fail-closed 跳过该出价(解码失败是事故,要告警);若有效,第 4 步应用 publisher restrictions(禁用 / 强制 consent / 强制 LI);第 5 步 vendor ID 在 vendorConsents 或 LI 里吗?——若否,跳过(vendor 未授权);若是,第 6 步每个所需 purpose 按其 GVL 声明的依据都 OK 吗?——若任一失败,降级为仅上下文(purpose 1、2);若全过,第 7 步你是否需要某个 special feature,如精确地理位置或指纹?——需要但没 opt-in 则跳过该能力;否则带全量个性化继续 bidder 收到请求时的流水线。红色节点是硬停,琥珀色节点是降级为上下文,绿色是唯一一条全量个性化的出口。可选的第 3 步 CMP-ID 检查为清晰起见省略了(见下面细节 #2)。注意顺序:publisher restrictions 在 vendor/purpose 之前——restriction 能翻转一个 vendor 声明的法律依据,所以先检查那个位,就是在检查错的位。

所需 purpose 集合取决于这家 vendor 实际在做什么。一个典型 DSP 的最低配置:

操作所需 purpose
接收 bid request、上下文出价1, 2
用第一方数据 / look-alike / 重定向1, 2, 3, 4
归因与效果测量1, 7
跨设备关联Feature 2 + 上述
用精确地理位置Special Feature 1 + 上述

四个安静地搞坏实现的细节

**1. vendor 检查放在 purpose 之前。**逻辑上这个检查是一个 AND——顺序不改变正确性。但运行上 vendor 优先更便宜:一次位查找,你的 vendor ID 是个常量,不匹配就短路掉整条请求。purpose 检查则是对着 GVL 声明走一遍数组,单请求成本更高。几乎所有生产代码都是 vendor.check() && purposes.check()

**2. CMP 检查确实是可选的。**你可以cmpId 去比对 IAB CMP List,确认字符串来自一个活跃、注册过的 CMP。实践中没人在热路径上这么做——IAB MO 在中心侧统一执行吊销,而给每条 bid request 加一次 CMP-list 查找带来的延迟,你不会想付。只有当你看到来自特定 CMP ID 的异常流量、想做防御式筛查时才加它。

**3. publisher restrictions 必须跑在 vendor/purpose 之前,而不是之后。**这是多数服务端搞错的那条规则。publisher restrictions 是覆盖——它能翻转一个 vendor 在 GVL 里声明的法律依据。如果某 vendor 把 purpose 2 声明在 LI 下,而媒体把它限制为 consent(类型 1),那检查 LI 位就是错的检查。先算出”这个媒体 × 这个 vendor × 这个 purpose 的有效法律依据”,再检查对应的位。

**4. special feature 有它自己的检查,不是默认的那个。**精确地理位置和主动指纹住在 SpecialFeatureOptIns 里,和 purpose 位分开。只在你真的需要那份数据时才检查它们。别把两个 special-feature 检查都塞进默认的 purpose 检查循环——对 99% 用不到它们的出价路径,那是白白浪费的 CPU。

最常见的那个合规失败:服务端代码检查了 vendor consent,却没检查 purpose consent。vendor 被授权了;用户从没同意过”建立画像”;DSP 却照样跑重定向。这是一次 GDPR 违规,不是一个边角情况。

6.4 兜底策略:几乎永远 fail-closed

当 TC String 缺失、解码失败,或解码出一个未授权状态时——你该怎么办?在 RTA 里这是个商业策略问题,有两个站得住脚的答案。在 TCF 里几乎从来不是。

场景推荐动作
gdprApplies == 0 或缺失当作非 GDPR 流量(信媒体的信号)
gdprApplies == 1、TC String 缺失fail-closed:跳过出价,或跑一条仅上下文的兜底(purpose 1、2)
TC String 在但解码失败fail-closed:这是事故级——告警并记日志
解码成功但 vendor consent = 0不出价
解码成功但 purpose consent 不全降级为仅上下文

和 RTA 里 fail-open 是个正当商业选择不同,TCF 基本没有任何站得住脚的 fail-open 场景。这里的 fail-open,除了极少数例外,都是一桩正在酝酿的合规违规——而最坏情况下的代价,是那种能终结职业生涯的罚单。

为什么 fail-closed 是法律 + 协议 + 系统设计,而不是『最佳实践』

AdTech 圈子里 fail-closed 常被当成一个保守的惯例传来传去。它不是。它被三个相互独立的方向各自要求,任何一个都足够:

**1. 法律层:问责原则(Accountability Principle)。**GDPR Art. 5(2):“The controller shall be responsible for, and be able to demonstrate compliance with”——每个处理数据的实体都必须能够证明自己合规。“我的上游没拒我”不是一个可被采纳的抗辩。Art. 26(共同控制者——连带责任)和 Art. 28(处理者义务)再堵死另一条明显的逃路:合规义务不可转嫁。你没法把它外包给那个把请求发给你的 SSP。

2. 协议层:IAB 把 fail-closed 编成了一条强制规则。TCF Policy §12(4)

If a Vendor is unable to read or process the contents of a received Signal, the Vendor must assume that it does not have permission to store and/or access information on a device, or to process personal data for any Purpose and/or Special Purpose.

——一个缺失的信号,按规定就是”否”。不是一条软建议。§12(3) 进一步禁止在实时决策里依赖缓存的”旧的同意”;§12(5) 让”无法合规”在技术上等价于”不要行动”。把整个 §12 读一遍——它就是 GDPR 问责原则翻译成 vendor 的具体技术义务。

3. 系统层:SSP 是一个不可信的中间方。用分布式系统的话讲,SSP 和你的 DSP 分处一道信任边界(trust boundary)的两侧。SSP 把请求转发给你,并不意味着同意在上游被核验过。这就是 defense in depth、don’t trust your inputs、fail-safe defaults、zero trust——在网络侧它对应端到端原则(end-to-end principle):端到端的语义检查必须由端点来做,中间方的工作不算数。把上游当成一个过滤器,是经典的反模式。

合起来,一句话:“上游转发了 ≠ 你被授权去处理。” SSP 把决策交给你不是 bug,而正是 GDPR 加 TCF 加可靠系统设计共同要求的。而你唯一被允许把它交回去的方向,是 skip_bid()

同样的原则,出现在任何真正的责任落在端点上的地方:

7. GPP:把 TCF 包进更大的信封

到 §6 结束,你已经能正确处理 GDPR 流量。但 TCF 只是一个法域。一家全球媒体至少要面对:

一家为每个法域都做一套独立传输的全球 DSP,会失去活下去的意志。IAB 在 2022 年推出 GPP(Global Privacy Platform),就是专门来阻止这件事的。

7.1 GPP 的设计:分节的信封

GPP 不是一个新的同意协议,而是一个信封格式——它把多个法域的同意字符串拼接起来,再前缀一个描述里面装了什么的头部。

GPP 信封结构:一个 GPP Header(如 DBABMA,其首字符 D 标记它是 GPP)用 &#x27;&#x27; 连到 Section 2(TCF EU v2,一条原始 TCF 字符串以 C 开头),再用 &#x27;&#x27; 连到 Section 6(US Privacy v1);另有一个 gpp_sid = [2, 6] 节点指向这两个 section,作为信封内含物的索引 GPP 是个信封,不是新的同意协议:各 section 用 ~ 连接,gpp_sid 是告诉你哪些 section 在场的索引。注意首字符的提示——GPP 字符串是 D,原始 TCF 字符串是 C

这个单字符的区别在实践里很有用——在任何生产日志里,瞄一眼首字符就知道你在看哪个协议。IAB 是故意把它做成兼容性标记的:base64 的 2 编码成 C3 编码成 D

每个 section 有一个对应法域的数字 ID。下面列出前十个——ID 11–27 沿着不断增长的美国州法名单继续往下(犹他、康涅狄格、佛州、蒙大拿、俄勒冈、得州等),完整表在 GPP Section Information 文档 里:

IDAPI 前缀描述
1tcfeuv1EU TCF v1(已废弃)
2tcfeuv2EU TCF v2——§3 讲的就是它
3GPP Header(必需)
4GPP signal integrity
5tcfcav1加拿大 TCF
6uspv1US Privacy(旧 CCPA 格式)
7usnatMSPA US National
8uscaCalifornia(CPRA)
9usvaVirginia
10uscoColorado
美国州法 section 一直延续到 ID 27(罗德岛)

除了独立的 section,GPP 还支持用 . 追加的可复用 subsection。实践中最重要的一个是 Global Privacy Control(GPC)——一个浏览器自身可经 Sec-GPC HTTP 头或 navigator.globalPrivacyControl 发出的 W3C 草案信号。CMP 会自动把它桥接进 GPP 字符串。**这意味着一个在浏览器里拨动了”do not sell”开关的用户,正向每一个 CMP 传递这个选择——他不必在每家媒体上分别点”拒绝”。**美国州监管者(尤其是加州)已开始把 GPC 当成一个具有法律约束力的 opt-out 信号,这让它在运营上变得重要。

7.2 gpp_sid:告诉你信封里装了什么的索引

gpp_sid 是一个数组,枚举这条 gpp 字符串里在场的 section ID:

{
  "regs": {
    "gpp": "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNY",
    "gpp_sid": [2, 6]
  }
}

这里信封同时携带 TCF EU v2(section 2)和 US Privacy v1(section 6)。跨法域用户——VPN 用户、旅行者、双居所媒体——会经常产出这种”叠加”的信封。

处理顺序:

1. 若 regs.gpp 存在且 gpp_sid 非空:
     解析 gpp_sid 列出的每个 section。
     找到与本次请求相关的 section(EU 用户找 section 2)。
     套用对应规则。
2. 否则回退到旧字段:
     regs.gdpr + user.consent(仅 TCF),或
     us_privacy 头(仅 CCPA)。

7.3 GPP 与 TCF 并存

那个标准困惑:**GPP 是不是取代了 TCF?**没有。

**TCF 仍是 EU/UK 的法域规则。**GPP 是一个传输与编码层,它的 EU section 里装着一条 TCF v2 字符串。生产里你会见到三种组合:

阶段bid request 里出现什么
Legacy(GPP 之前)只有 regs.ext.gdpr + user.ext.consent(或 OpenRTB 2.6 的一等字段版本)
过渡期TCF 字段 regs.gpp + gpp_sid 都在,内容相同
GPP-only(较新的 SSP)只有 regs.gpp + gpp_sid;TCF 字段省略——你必须从 GPP 信封里抽出 TC String

生产代码三种都得处理:

function extractTcfTcString(bidRequest: any): string | undefined {
  const gpp = bidRequest?.regs?.gpp;
  const gppSid: number[] | undefined = bidRequest?.regs?.gpp_sid;

  if (gpp && gppSid?.includes(2)) {
    return extractSectionFromGpp(gpp, 2);
  }

  return bidRequest?.user?.consent ?? bidRequest?.user?.ext?.consent;
}

7.4 客户端:window.__gpp

类比 __tcfapi,IAB 定义了 window.__gpp

window.__gpp('getGPPData', function(gppData, success) {
  // gppData.gppString          — 完整 GPP 字符串
  // gppData.applicableSections — 如 [2, 8]
  // gppData.parsedSections     — 各 section 解析后的对象
});

window.__gpp('addEventListener', function(data, success) {
  // 订阅同意变化
});

主流 CMP——OneTrust、Sourcepoint、Didomi、Usercentrics——都同时暴露 __tcfapi__gpp,通常背后是同一套状态模型、两个 API 面。

一句话记住 GPP:它只换了信封,没换信。EU section 里仍是那条 TCF v2 字符串——你在 §3–§6 写的解码与决策逻辑原样复用,只是多了一步”从信封里取出 section 2”。

8. 生产环境的坑

事故案例里反复出现的那些模式。

8.1 GVL 版本不一致

GVL 大约每月发一个新版本。忘记同步的 vendor 会产出一个慢性 bug:

修法:每天同步最新 GVL、缓存它,并且在解码时,去查 TC String 自己声明的那个 GVL 版本(VendorListVersion——而不是你当前的版本。历史版本全都保留在 https://vendor-list.consensu.org/v3/archives/vendor-list-v<N>.json

8.2 缓存 TTL 活过了有效期

TC String 反映的是用户在某一刻的状态。用户可能立刻就:

如果你的 bidder 缓存了 TC String 或任何由它派生的标志,缓存 TTL 必须短于 TC String 的有效期(IAB 建议最长 13 个月,但多数 CMP 默认用 6 个月)。

经验法则:把同意派生的缓存 TTL 压到 1 小时及以下。每条 bid request 都应当从它实际带来的那条 TC String 重新派生决策,而不是从跨请求的粘性状态里取。

8.3 gdprApplies 的三态陷阱

在 CMP API 里,gdprAppliestrue | false | undefined

IAB 对 undefined 的指引:当作 true。这里保守是对的。当它经 OpenRTB 传递时,对应状态是”regs.gdpr 缺失”,你的 bidder 应当当作 GDPR 适用。

最常见的那个 bug:undefined → false 映射,给后来证明是欧盟用户的人处理了数据,几个月后法务来要证据时才发现问题。

8.4 滥用 Legitimate Interest

在 TCF v2.0 早期,vendor 发现他们几乎可以把每个 purpose 都声明在 LI 下——“LI 默认开,用户得主动反对,通过率漂亮极了”。这扇门在 v2.2 被大幅关上:

今天的安全假设:任何 LI 声明都得经得起法律审视。重定向和画像建立只能走 consent——别想着把它们绕道 LI。

8.5 忽略 Publisher Restrictions

读了 PurposesConsentVendorConsents、却跳过 PublisherRestrictions 的服务端实现,大概率会处理媒体明确叫你别处理的数据。那既是对媒体的违约,很可能是一次 GDPR 违规。

修法:先读 publisher restrictions、应用覆盖,再检查对应的位。

8.6 GPP 与 TCF 互相矛盾

过渡期里最常见的诡异状态:一条 bid request 同时带 user.consent(TCF)和 regs.gpp(含一个 TCF section 的 GPP 信封),而内容不同。通常是 SSP 侧的 bug——SSP 把一条 TC String 复制进了两个位置,但同意变化时只更新了一个。

保守的解法:优先用 regs.gpp(SSP 正在积极维护的较新标准),但记录每一次不一致,好拿去找 SSP。

8.7 iOS ATT 与 TCF 相互独立

App 开发者经常把 iOS ATT 和 TCF 混为一谈。它们是相互独立的法域规则:

但在欧盟,iOS App 必须两者都满足:用户必须 (1) 在 ATT 提示里允许追踪,并且 (2) 在 CMP 里授予 TCF 同意。任一为否,IDFA 就无法用于跨 App 用途。

ATTTCF consent能力
允许允许完整功能
允许拒绝IDFA 可访问,但法律上不可用
拒绝允许没有 IDFA——仅上下文 + SKAN
拒绝拒绝同上——仅上下文 + SKAN

9. 测试与可观测性

9.1 工具

9.2 值得告警的指标

指标你在盯什么经验阈值
gdprApplies == 1 占总流量比骤降 = CMP 误触发偏离基线 ±10%
TC String 解码失败率应为 0;任何上升都是事故> 0.1%
TC String 缺失(在 gdprApplies = 1 时)CMP 没写,或 SSP 没读> 1%
你这家 vendor 的 consent 通过率下跌 = CMP UI 的”全部拒绝”更显眼了,或你的 GVL 声明把 vendor 排除了跌 > 5%
你这家 vendor 的 LI 通过率同上跌 > 5%
逐 purpose 的 consent 通过率找出哪个 purpose 是你的瓶颈跌 > 5%
GPP 出现率跟踪 SSP 侧 GPP 铺开进度单调上升
GPP 与 TCF 不一致率见 §8.6> 1%

9.3 离线对账

每月一次,拿同一组字符串,把你服务端解码的输出和 IAB validator 解码的输出对账一遍。100% 一致,没有例外。任何不一致都是一桩潜伏的合规事故。

10. FAQ

Q1:我们不是 TCF vendor——我们只是把 DSP 当客户端用。为什么要关心 TCF?

因为当你上传受众、配置转化像素,或在 DSP 上搭重定向名单时,你的法律暴露在控制者那一侧——也就是你,广告主。懂 TCF 能告诉你:

Q2:用户点了”全部拒绝”之后,我们还能做什么?

你可以做:

你不能做:

Q3:“全部拒绝”在 CMP UI 里真的是强制的吗?

是的。TCF v2.2 强制要求”全部接受”和”全部拒绝”在 CMP UI 的初始层上以同等显著度出现——这是 v2.2 较有影响的改动之一。它直接把那种”拒绝”藏进二级菜单、“接受”是个巨大绿按钮的暗黑模式设计判为违法。

Q4:如果我们永远拿不到 TC String,是不是就被彻底锁在出价之外了?

不一定。如果媒体判定非个性化广告是合适的,DSP 可以跑一条上下文路径(purpose 1、2)——但这条上下文路径必须完全不碰任何用户级数据,包括 cookie、设备 ID 和 IP(在 GDPR 下,IP 是 PII)。

实践中成熟的 DSP 会维护两条相互独立的代码路径:

这里真正难的工程问题,是让两条路径都带着各自的兜底在生产里跑通,而不让彼此的数据交叉污染。

11. 速查表

12. 收尾

TCF 是那种典型的”实现简单、合规难”的协议。解码逻辑是一晚上的活儿。围着它的其余每一个工程决策——兜底策略、缓存 TTL、GVL 同步、vendor / publisher restriction 的优先级、GPP 过渡兼容——都直接接进法律暴露或商业收入。

一句话蒸馏:

协议是工程师的合同。合同被违反时,账单由法务来付。

别想绕过 TCF。别 fail-open。别拿一个 fail-closed 默认值去换那点边际增量流量。这不像 RTB 栈里别处那些软优化问题——在这儿出错的代价不是几次丢出价,而是六个月后以一封 CNIL(或下一个同类监管者)来信的形式交付的、全球年营收的一个百分比。**Criteo 的 4000 万欧元不是一个 bug,而是一次文档(证据链)的失败。**而 TCF 里的文档失败,意味着某处某人,fail-open 了。

把 §3、§4、§6、§8 弄懂,你就能负责任地在团队里跑起这套。§7(GPP)和 §9(可观测性)会在你公司走向全球、或监管堆上再添一部法律的那一刻起变成非做不可——而按过去这几年看,那是每六个月就会发生一次的事。


延伸阅读

先看同系列里和本篇直接相邻的几篇:

再是规范与一手资料:

法律 / 政策

协议 / 技术规范

工具 / 实现

相关 / 相邻

附录:术语表


views
Share this post on:

Previous Post
冷启动:程序化栈每一层都在解同一个问题
Next Post
推荐全链路离线评估:单看召回 HitRate 或排序 AUC 都会骗你