2023 年 6 月,法国数据保护机构 CNIL 对 Criteo 开出了一张高达 4000 万欧元 的罚单。其核心理由并非指控 Criteo 存在主观恶意,而是指出它无法证明被重定向的用户真正授予了同意。换句话说,对于任意一次具体的广告竞价,Criteo 都拿不出一条完整且可审计的证据链来证明:屏幕背后的那个用户确实说过「可以」。
这正是 IAB TCF(Transparency and Consent Framework)被设计出来所要解决的核心痛点。
在整个广告技术(AdTech)生态中,TCF 属于那种极为罕见的协议——它表面上看起来像是一个普通的传输格式,但其本质却是一份严谨的法律合同。具体而言,它将 GDPR 中那条抽象的法律义务(即「只有在具备合法性基础时才能处理个人数据」)巧妙地编码成了一串 Base64 字符。正因如此,上百家需求方与供给方(DSP & SSP)能够在 100 毫秒的极短时间内,通过 OpenRTB 协议将其在整个供应链中传递。在这个过程中,链路上的每一家公司都能独立判断:眼前这个用户,是否真正同意了我接下来要执行的数据处理操作。
本文是一份专为在线工程师准备的协议实操走查。我们将深入探讨:CMP(Consent Management Platform)在页面上具体做什么、一条 TC String 里面到底打包了哪些信息、它又是如何经由 OpenRTB 协议在链路中传递的。同时,文章还将详细解析 bidder 在服务端收到请求后必须执行的授权检查逻辑,以及 GPP(Global Privacy Platform)是如何作为全球合规信封将 TCF 等区域性协议包裹其中的。需要强调的是,这不是一篇空泛的法律科普,文中的每一个小节都精准落在了 bidder、ad server 或测量供应商必须落地实现的工程细节上。
范围说明:在之前的 App 广告 SDK 一文中,我曾将端上的同意管理与合规一笔带过,并承诺「协议基础另文展开」——本文正是那块缺失的拼图。对比前一种方案,在 RTA 架构中「fail-open 是一个正当的商业选择」;然而在 TCF 的世界里,fail-open 几乎永远是一桩正在酝酿的合规事故。下文的 §6.4 将会专门讲清这一关键对照。
本文是 隐私与合规 系列的第 2 篇(同意框架)。 全系列 3 篇:
- 后 Cookie 身份层
- 深挖 IAB TCF 与 GPP
- 地区广告政策与合规
一句话定位:TCF 把「有没有同意」编成一条可在 OpenRTB 里传递的 TC String,bidder 必须按 purpose 做授权检查,不能 fail-open。
TL;DR
- TCF 是一份标准化的同意合同,不是一个传输格式:它解决了一个非常具体的 GDPR 痛点。一次广告曝光(Impression)通常会流经几十家公司,每一家都需要独立、实时地确认该用户是否同意了对其数据的使用。没有 TCF 时,每家只能自建孤岛;引入 TCF 后,整个生态共享一份 GVL(Global Vendor List)和一条 TC String。
- TC String 是物理载体:它是一个经过 base64url 编码、按段(segment)切分的位域(bitfield)。其 Core 段封装了 11 个 purpose 的同意(consent)或正当利益(Legitimate Interest,简称 LI)位,外加一张按 vendor ID 索引的位图,用于明确标明谁被授权。
- OpenRTB 用四个字段携带它,这就是全部的线级集成:
regs.ext.gdpr(在 2.6 版本中为regs.gdpr)、user.ext.consent(或user.consent)、regs.gpp以及regs.gpp_sid。其余所有的合规逻辑,都是这四个字段的下游延伸。 - GPP 不是 TCF 的继任者,而是一个信封:GPP 巧妙地将 TCF(涵盖 EU/UK)、US Privacy,以及越堆越高的美国各州隐私法律,统一打包进一条
gpp字符串中。这使得全球的 DSP 不必再为每个司法辖区单独开发一套传输机制,而gpp_sid则会清晰地告诉你信封里到底装了什么。 - 服务端的决策本质上是三步检查:解码 TC String → 核验你的 vendor ID 是否被授权 → 核验你需要的 purpose 是否被授权。需要警惕的是,第一步出错将导致严重的合规事故,而第二步出错则意味着你会直接丢掉这次出价机会。
怎么读这篇(按角色取所需):对于赶时间的工程师,建议直接阅读 §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 行业立刻发现自己陷入了处境尴尬的泥沼。
一次普通的广告曝光(Impression)往往会触及几十家公司——包括媒体(Publisher)、SSP、ad exchange、DSP、DMP、CDP,以及各类测量和验证供应商、CMP。在这条复杂的供应链中,任何一家都可能在某个环节「处理个人数据」。GDPR 明确要求,每一次数据处理行为都必须建立在一个坚实的合法性基础之上。而在广告商业化变现的真实场景里,真正用得上的合法性选项其实只有两个:同意(consent,对应 Art. 6(1)(a))和正当利益(legitimate interest,对应 Art. 6(1)(f))。
这一法律要求立刻在工程侧引发了三个棘手的挑战:
- 逐 vendor 的粒度:一次广告曝光(Impression)触及 30 家公司,意味着每一家都必须确切知道,用户是否专门对它说了「可以」。
- 逐 purpose 的粒度:不同的公司获取数据有着截然不同的目的——比如归因、重定向、受众测量或是反作弊。同一个用户完全可能对 A 公司的目的表示同意,却对 C 公司的目的予以拒绝。
- 100ms 的线级预算:整个交易链路的传输高度依赖 OpenRTB 协议。在实时竞价(RTB)那严苛的 100 毫秒延迟预算内,根本没有多余的空间为每条 bid request 附上 30 份独立的同意声明。
面对这些痛点,IAB 给出的终极答案就是 TCF。它引入了一份标准化的统一 vendor 注册表,外加一张按 vendor ID 精确索引的位图。通过这种机制,这 30 家公司得以在极短的时间内共享同一份序列化的同意状态。
整套框架稳稳地立在三块基石之上:
| 支柱 | 是什么 | 由谁维护 |
|---|---|---|
| GVL(Global Vendor List) | 一个 JSON 文件,详细列出每一家 TCF 注册 vendor,各有唯一 ID,并声明它需要哪些 purpose / feature | IAB 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. 四个角色与数据流
生产者 → 产物 → 消费者的流向:CMP 产出 TC String,页面通过
__tcfapi 暴露它,每个下游 vendor 各自独立解码、各自判断自己是否被授权行动。最终判断永远是 vendor AND purpose——把这个 AND 搞错,是最常见的合规失败(见 §6.3)。
在 TCF 的运转逻辑中,主要涉及四个核心角色:
- 媒体(Publisher):站点或 App 的实际运营方。在 GDPR 框架下,其法律身份是数据控制者(data controller),肩负着部署 CMP、确保正确的告知义务落实到位的重任。
- CMP:这是一段运行在媒体侧的 JavaScript SDK(或 App 库)。它的职责是弹出同意 UI、收集用户的选择、生成 TC String,并将其妥善持久化到第一方 cookie、localStorage 或 App 存储中。需要强调的是,每个 CMP 都必须向 IAB Europe 注册,从而拿到一个专属的数字 CMP ID。
- 供应商(Vendor):涵盖了 DSP、SSP、测量方、验证方、ad server 以及 DMP——简而言之,就是链路上任何处理用户数据的角色。每一个合规的 vendor 在 GVL 里都拥有一个唯一的 vendor ID。
- 用户(User):协议的绝对主体。TC String 中的每一个同意位,都是严格相对于单个具体用户而言的。
需要强调的是,媒体本身也是一个特殊的 vendor。在 TC String 里,有一个专门的段被称为 Publisher TC,里面装载的是用户对媒体自身的用途的同意状态(这完全独立于第三方 vendor 的同意)。然而在实际开发中,它往往是服务端代码最容易忽略其存在的那个段。
3. TC String:协议层的解剖
TC String 就是那个在整个链路中穿梭的传输格式。一条真实的 TC String 通常长这样:
CQi298AQi298AEsACCENCbFoAP_gA...AA.ILCJB_C7NbXFmybZ36...
各个段之间通过 . 进行拼接。关于哪些段是必需的、哪些是可选的,规则曾随着版本更迭发生过变化。截至 2025 年 4 月的 v2.3 规范更新,现行的硬性规则如下:
| 段 | 必需? | 内容 |
|---|---|---|
| Core String | 必需 | 元数据 + purpose 的 consent / LI + vendor 的 consent / LI + publisher restrictions |
| Disclosed Vendors | v2.3 起必需 | CMP 实际向用户披露过的那组 vendor(此举彻底解决了 special-purpose-only vendor 长期以来的一处歧义) |
| Publisher TC | 可选 | 媒体自身的 purpose 同意,包含任何自定义 purpose |
TC String 是各段用字面量
. 拼接而成。Core 永远在;Disclosed Vendors 在 v2.3 变成强制;Publisher TC 可选,也是服务端代码最常忘记去读的那个段。
如果你的服务端代码编写于 2025 年 4 月之前,那么单段字符串(即只有 Core 段)仍然能够被解码,但在形式上已经不再完整。现代的 CMP 都会发出两段或三段的字符串。
3.1 VendorConsents:bitfield 与 range 的取舍
在整个规范中,工程上最有意思的细节莫过于 VendorConsents(以及与之对应的 VendorLegitimateInterests)的编码方式。该段内设有一个名为 IsRangeEncoding 的标志位,用于在两种截然不同的布局之间灵活切换:
- Bitfield 编码:这是一个长度为
MaxVendorId的扁平位数组。当第i位置为 1 时,即表示 vendor IDi获得了授权。 - Range 编码:这是一个变长的区间列表,例如
[1–5, 7, 9–20, 50–100]。
究竟哪种方式更短,完全取决于被授权集合的稀疏程度:
| 场景 | 更短的编码 |
|---|---|
| 用户点了「全部接受」 → 几乎每个 vendor ID = 1 | Bitfield(一长串 1) |
| 用户点了「全部拒绝」 → 几乎每个 vendor ID = 0 | Bitfield(一长串 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,你会得到大致如下的解析结果:
| 字段 | 值 |
|---|---|
version | 2 |
cmpId | 300(Sourcepoint) |
cmpVersion / consentScreen / consentLanguage | 2 / 2 / EN |
vendorListVersion | 155——你的服务端应当拉取 GVL v155 才能正确解读这条字符串,详见 §8.1 |
policyVersion | 5——v2.2 上线时是 4;随 GVL v3 一起升到了 5。务必动态读取它,切忌硬编码 |
isServiceSpecific | true |
publisherCountryCode | CY(塞浦路斯) |
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 规范之后,这种诱导性的 UI 设计本身就已经构成了一项严峻的合规风险(详见 §8)。
4. 目的、特征与合法性基础
根据现行的 IAB Europe TCF Policies, Appendix A,TCF v2.2/v2.3 严谨地定义了 11 个 Purpose、3 个 Special Purpose、3 个 Feature 以及 2 个 Special Feature。vendor 必须在 GVL 里如实声明自己需要哪些权限,而最终的决定权则通过 CMP 交还给用户。
4.1 11 个 Purpose(取自 Appendix A 原文)
| ID | 官方名称 | 一句话是什么 |
|---|---|---|
| 1 | Store and/or access information on a device | Cookie、localStorage、设备 ID。唯一允许的合法性基础:Consent。这是 Appendix A 里唯一明确写死法律依据的 purpose——它直接对应 ePrivacy Directive Art. 5(3)。 |
| 2 | Use limited data to select advertising | 非个性化(上下文)选广告,外加 frequency capping |
| 3 | Create profiles for personalised advertising | 为广告定向建立逐用户画像 |
| 4 | Use profiles to select personalised advertising | 用画像去定向某个用户 |
| 5 | Create profiles to personalise content | 同 3,用于编辑内容 |
| 6 | Use profiles to select personalised content | 同 4,用于编辑内容 |
| 7 | Measure advertising performance | 归因、转化、可见性 |
| 8 | Measure content performance | 内容侧的同上 |
| 9 | Understand audiences through statistics or combinations of data from different sources | 受众测量、面板派生的人口统计 |
| 10 | Develop and improve services | 产品改进、ML 模型训练 |
| 11 | Use limited data to select content | v2.2 新增——purpose 2 在内容侧的上下文对应物 |
4.2 3 个 Special Purpose
需要特别说明的是,special purpose 只能在正当利益(LI)的框架下进行处理——用户无法对其予以拒绝。背后的逻辑非常现实:如果没有这些基础权限,平台要么彻底瘫痪,要么就会违反其他法律法规。
| ID | 官方名称 | 现实例子 |
|---|---|---|
| 1 | Ensure security, prevent and detect fraud, and fix errors | IVT 检测、反 bot、错误恢复 |
| 2 | Deliver and present advertising and content | 把广告字节从服务器搬到用户——用到 IP、UA、屏幕尺寸这些你压根跳不过的东西 |
| 3 | Save and communicate privacy choices | 元目的:持久化并传递 TCF 状态本身。CMP 和 vendor 用它来读写 TC String |
4.3 Feature 与 Special Feature
Feature(3 个)——vendor 声明它们是追求某个 purpose 时所用的手段,没有单独的用户勾选项,但要披露:
| ID | 官方名称 |
|---|---|
| 1 | Match and combine data from other data sources |
| 2 | Link different devices |
| 3 | Identify devices based on information transmitted automatically(经 IP / UA / TCP 信号的被动指纹) |
Special Feature(2 个)——需要用户显式 opt-in,而不只是泛泛的同意:
| ID | 官方名称 |
|---|---|
| 1 | Use precise geolocation data(精度低于 500m,或经纬度小数点后多于 2 位) |
| 2 | Actively scan device characteristics for identification(主动指纹——字体、插件、屏幕分辨率) |
Feature 3 与 Special Feature 2 之间的界限极其关键。前者代表「我们接收并使用自动到达的设备信息」,这仅仅需要披露即可。而后者则意味着「我们主动探测设备的可识别特征」,这必须获得用户的显式 opt-in。正因如此,如果你的产品涉及任何形式的指纹识别,你必须清醒地知道自己究竟站在哪一侧。因为在多数 CMP UI 中,special feature 通常被折叠进了二级菜单,导致其 opt-in 率惨不忍睹。可以断言,在欧盟试图把商业模式建立在 Special Feature 2 之上,基本是一条死胡同。
4.4 Consent vs Legitimate Interest:逐目的检查
对于每一个 purpose,最多存在两种可能的合法性基础(注意,v2.2 已经强制关闭了好几个选项):
- Consent——用户显式 opt-in。对应的状态位存储在
PurposesConsent里。 - Legitimate Interest——vendor 主张 LI,同时用户保留反对的权利。对应的状态位存储在
PurposesLITransparency里。
针对单个 purpose,正确的服务端判断逻辑应当如下:
对我需要的每个 purpose P:
在 GVL 里查这个 vendor 为 P 声明的法律依据。
声明 consent → 检查 PurposesConsent[P] == 1
声明 LI → 检查 PurposesLITransparency[P] == 1
声明 flexible → 看默认值,再叠加任何
publisher restriction 的覆盖
未声明 → fail
令人遗憾的是,这是 TCF 落地过程中被实现得最离谱的一环。常见的失败模式包括:
- vendor 在 GVL 条目里把某 purpose 声明为 LI,但服务端却只去检查
PurposesConsent。由于用户从未显式同意,consent 位正确地显示为 0,服务端便错误地将请求视为未授权,导致广告预算和收入悄悄流失。 - 反之,vendor 把某 purpose 声明为 Consent,但服务端却去检查
PurposesLITransparency。由于 LI 位默认可能是 1,服务端便违规处理了它本无权处理的数据。这就是彻头彻尾的合规事故。
铁律只有一条:永远相信 vendor 的 GVL 声明,千万别相信你的直觉。
4.5 Publisher restrictions:覆盖机制
媒体拥有至高无上的权利,可以在逐 purpose 层面强行覆盖一个 vendor 所声明的法律依据。目前存在三种 restriction 类型:
| 类型 | 含义 |
|---|---|
| 0 | vendor 完全不得使用此 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-Specific(IsServiceSpecific = 1) | TC String 只对当前媒体 / 媒体组有效,存在媒体自己的域名下 | 唯一合法模式 |
Global(IsServiceSpecific = 0) | 经 consensu.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 |
cmpId、cmpVersion | CMP 标识 |
gdprApplies | true / false / undefined |
eventStatus | tcloaded | cmpuishown | useractioncomplete |
cmpStatus | stub | loading | loaded | error |
listenerId | 供 removeEventListener 用 |
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.restrictions | publisher 覆盖 |
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.consent 和 user.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):
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 + 上述 |
四个安静地搞坏实现的细节
-
vendor 检查放在 purpose 之前。从逻辑上讲,这个检查是一个 AND 操作——顺序并不会改变最终的正确性。但在实际运行中,vendor 优先显然更便宜:它只是一次简单的位查找,你的 vendor ID 是个常量,一旦不匹配就能迅速短路掉整条请求。相比之下,purpose 检查则需要对着 GVL 声明走一遍数组,单请求的计算成本更高。因此,几乎所有生产代码采用的都是
vendor.check() && purposes.check()。 -
CMP 检查确实是可选的。你完全可以拿
cmpId去比对 IAB CMP List,以确认字符串确实来自一个活跃且注册过的 CMP。但在实践中,几乎没人在热路径上这么干——因为 IAB MO 会在中心侧统一执行吊销操作,而给每条 bid request 强加一次 CMP-list 查找所带来的延迟,是你绝对不想承受的。只有当你观察到来自特定 CMP ID 的异常流量,想要做防御式筛查时,才需要加上它。 -
publisher restrictions 必须跑在 vendor/purpose 之前,而不是之后。这正是多数服务端容易搞错的那条铁律。publisher restrictions 的本质是覆盖——它拥有翻转一个 vendor 在 GVL 里声明的法律依据的权力。如果某 vendor 把 purpose 2 声明在 LI 下,而媒体却把它限制为 consent(类型 1),此时如果你直接去检查 LI 位,那就是在检查一个错误的位。正确的做法是:先算出「这个媒体 × 这个 vendor × 这个 purpose 的有效法律依据」,然后再去检查对应的位。
-
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 经常被误认为是一个保守的惯例。事实并非如此。它被三个相互独立的方向各自强硬要求,任何一个理由都足够充分:
-
法律层:问责原则(Accountability Principle)。GDPR Art. 5(2) 明确规定:“The controller shall be responsible for, and be able to demonstrate compliance with”——每个处理数据的实体都必须能够证明自己合规。「我的上游没拒我」从来不是一个可被法庭采纳的抗辩理由。Art. 26(共同控制者——连带责任)和 Art. 28(处理者义务)更是彻底堵死了另一条明显的逃路:合规义务绝对不可转嫁。你根本没法把它外包给那个把请求发给你的 SSP。
-
协议层: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 具体技术义务的完美镜像。
- 系统层: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()。
同样的原则,也广泛出现在任何真正将责任压在端点上的领域:
- 金融风控:对手方给你打了钱,并不等于这钱是干净的——你仍要跑 KYC。
- 供应链合规:供应商发了货,并不等于货是合规的——你仍要报关。
- API 安全:上游网关说请求有效,并不等于你的服务可以信它——你要重新校验。
7. GPP:把 TCF 包进更大的信封
读到 §6 结束,你已经具备了正确处理 GDPR 流量的能力。但必须清醒地认识到,TCF 仅仅只是一个法域。一家志在全球的媒体,至少要面对以下错综复杂的局面:
- EU/UK GDPR(TCF 正是为之量身定制的法域)
- California CPRA(CCPA 的强力升级继任者)
- 一串不断变长的美国州法——弗吉尼亚、科罗拉多、康涅狄格、犹他、得州、佛州、蒙大拿、俄勒冈、特拉华、爱荷华、内布拉斯加、新罕布什尔、新泽西、田纳西、明尼苏达、马里兰、印第安纳、肯塔基、罗德岛,而且每个立法季这个名单还在不断增加
- 加拿大 PIPEDA / 魁北克 Law 25
- 巴西 LGPD、中国 PIPL,等等
如果一家全球 DSP 试图为每个法域都单独开发一套独立的传输机制,那它大概率会面临灾难性的开发与维护成本。正是为了阻止这种碎片化,IAB 在 2022 年果断推出了 GPP(Global Privacy Platform)。
7.1 GPP 的设计:分节的信封
需要明确的是,GPP 本身并不是一个新的同意协议,而是一个极其巧妙的信封格式——它将多个法域的同意字符串无缝拼接起来,并在最前面加上一个描述里面装了什么的头部。
GPP 是个信封,不是新的同意协议:各 section 用
~ 连接,gpp_sid 是告诉你哪些 section 在场的索引。注意首字符的提示——GPP 字符串是 D,原始 TCF 字符串是 C。
这个单字符的区别在日常排障中非常实用——在任何生产日志里,只需瞄一眼首字符,你就能立刻判断出自己正在看哪个协议。IAB 是故意把它设计成兼容性标记的:base64 的 2 编码成 C,而 3 则编码成 D。
每个 section 都拥有一个对应法域的数字 ID。下面列出前十个——ID 11–27 则沿着不断增长的美国州法名单继续往下延伸(犹他、康涅狄格、佛州、蒙大拿、俄勒冈、得州等),完整的列表可以在 GPP Section Information 文档 里找到:
| ID | API 前缀 | 描述 |
|---|---|---|
| 1 | tcfeuv1 | EU TCF v1(已废弃) |
| 2 | tcfeuv2 | EU TCF v2——§3 讲的就是它 |
| 3 | — | GPP Header(必需) |
| 4 | — | GPP signal integrity |
| 5 | tcfcav1 | 加拿大 TCF |
| 6 | uspv1 | US Privacy(旧 CCPA 格式) |
| 7 | usnat | MSPA US National |
| 8 | usca | California(CPRA) |
| 9 | usva | Virginia |
| 10 | usco | Colorado |
| … | … | 美国州法 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 v320 把 vendor X 的某 purpose 从 LI 重新归类为了 consent。
- 你的服务端却依然引用着 v315 的陈旧快照。
- 用户的 CMP 正常生成了一条基于 v320 的 TC String。
- 最终,你的授权逻辑和 CMP 对世界的认知彻底脱节了。
修法:务必每天定时同步最新的 GVL 并缓存它。更重要的是,在解码时,一定要去查 TC String 自身声明的那个 GVL 版本(即 VendorListVersion)——而不是简单粗暴地使用你当前的版本。所有的历史版本都被妥善保留在 https://vendor-list.consensu.org/v3/archives/vendor-list-v<N>.json。
8.2 缓存 TTL 活过了有效期
TC String 仅仅反映的是用户在某一特定时刻的状态。用户完全可能在下一秒就:
- 重新打开 CMP 二级菜单、修改之前的选择。
- 半年后再次访问,遇到一个重新弹出的 CMP 提示。
- 更换设备(请牢记,同意是逐设备绑定的,而不是逐用户)。
如果你的 bidder 缓存了 TC String 或任何由它派生的标志,那么缓存的 TTL 必须严格短于 TC String 的有效期(IAB 建议最长 13 个月,但多数 CMP 默认采用 6 个月)。
经验法则:将由同意派生出的缓存 TTL 极限压缩到 1 小时甚至更低。每条 bid request 都应当从它实际带来的那条 TC String 中重新派生决策,而不是依赖跨请求的粘性状态。
8.3 gdprApplies 的三态陷阱
在 CMP API 中,gdprApplies 存在三种状态:true | false | undefined:
true——用户明确在 GDPR 的管辖范围内。false——用户在范围外。undefined——CMP 暂时无法确定(可能是地理查找失败,或是 IP 解析超时等)。
IAB 对 undefined 的官方指引非常明确:一律当作 true 处理。这种保守策略是极其正确的。当它经由 OpenRTB 传递时,对应的状态是「regs.gdpr 缺失」,此时你的 bidder 应当毫不犹豫地将其当作 GDPR 适用。
最常见的那个致命 bug 是:开发者错误地进行了 undefined → false 的映射,结果给后来被证明是欧盟用户的人违规处理了数据。直到几个月后法务来索要证据时,这个漏洞才被惊恐地发现。
8.4 滥用 Legitimate Interest
在 TCF v2.0 的早期阶段,vendor 们惊喜地发现,他们几乎可以把每个 purpose 都堂而皇之地声明在 LI 下——「LI 默认开启,用户必须主动反对,通过率简直漂亮极了」。然而,这扇后门在 v2.2 版本被大幅封死:
- purpose 3、4、5、6 彻底不再允许使用 LI。
- vendor 必须为每一个 LI purpose 提供详尽、具体的书面理由,并由 IAB 进行严格审核。
今天的安全假设:任何 LI 声明都必须经得起严苛的法律审视。重定向和画像建立只能老老实实走 consent 路径——千万别再妄想把它们绕道 LI。
8.5 忽略 Publisher Restrictions
如果一个服务端实现认真读取了 PurposesConsent 和 VendorConsents,却偏偏跳过了 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 混为一谈。实际上,它们是两套完全相互独立的法域规则:
- ATT(App Tracking Transparency) 是 Apple 独家推行的机制,主要控制 App 对 IDFA 的访问权限,它和 GDPR 或 TCF 没有任何直接关系。
- TCF 则是 IAB 牵头制定的合规框架,同样和 Apple 的 ATT 互不干涉。
但在欧盟市场,iOS App 必须同时满足这两道门槛:用户必须 (1) 在 ATT 提示里允许追踪,并且 (2) 在 CMP 里授予 TCF 同意。只要其中任何一项为否,IDFA 就绝对无法用于跨 App 用途。
| ATT | TCF consent | 能力 |
|---|---|---|
| 允许 | 允许 | 完整功能 |
| 允许 | 拒绝 | IDFA 技术上可访问,但法律上绝对不可用 |
| 拒绝 | 允许 | 没有 IDFA——仅能走上下文 + SKAN 路径 |
| 拒绝 | 拒绝 | 同上——仅能走上下文 + SKAN 路径 |
9. 测试与可观测性
9.1 工具
- IAB TCF Compliance Validator——只需贴进一条 TC String,就能清晰地看到每个解码后的字段。这是开发期不可或缺的神器。
@iabtechlabtcf/cmpapi——这是 IAB 官方提供的参考 CMP API 实现。你可以在本地用它起一个假 CMP,把你的广告 SDK 对着每种同意状态彻底测一遍。- Charles / mitmproxy——用于拦截 OpenRTB 请求,精准核验
regs.gdpr/user.consent/regs.gpp是否如预期般出现。 - vendor 的 staging 模式——主流如 OneTrust、Sourcepoint、Didomi 都支持能模拟欧盟 IP、强制触发 CMP UI 的 staging 环境。
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 能清晰地告诉你:
- 你的哪些转化记录是真正在 TCF 合规同意下采集的(而哪些绝对不是)。
- 为什么你辛苦上传的受众,有时在 DSP 套用其同意过滤后会大幅缩水。
- 当法务严厉地问「我们的重定向名单合规吗」时,你该如何给出无懈可击的回答。
Q2:用户点了「全部拒绝」之后,我们还能做什么?
你可以做:
- 上下文定向——基于媒体内容来选择合适的广告。
- 当前会话内的 frequency capping(前提是不使用任何持久标识)。
- 诚实的 SKAN / 仅 postback 归因。
你绝对不能做:
- 任何形式的重定向或 look-alike 建模。
- 跨 App 或跨站的画像建立。
- 任何向设备写入持久标识的行为——除非它严格落在 purpose 1 那几个极其狭窄的豁免范围内。
Q3:「全部拒绝」在 CMP UI 里真的是强制的吗?
是的。TCF v2.2 强制要求「全部接受」和「全部拒绝」在 CMP UI 的初始层上必须以同等显著度出现——这是 v2.2 最具影响力的改动之一。它直接将那种把「拒绝」深藏进二级菜单、而「接受」却是个巨大绿按钮的暗黑模式设计判为了违法。
Q4:如果我们永远拿不到 TC String,是不是就被彻底锁在出价之外了?
不一定。如果媒体判定非个性化广告是合适的,DSP 完全可以跑一条上下文路径(仅限 purpose 1、2)——但这条上下文路径必须绝对不碰任何用户级数据,包括 cookie、设备 ID 甚至 IP(请注意,在 GDPR 框架下,IP 属于 PII)。
在实践中,成熟的 DSP 都会精心维护两条相互独立的代码路径:
- 完整路径——同意在场,全量执行重定向 + 画像 + 测量。
- 上下文路径——同意缺失或部分缺失,只使用媒体提供的页面级信号(如 URL、内容类别、版位元数据),且绝不写入任何持久标识。
这里真正硬核的工程挑战,是如何让这两条路径都带着各自的兜底逻辑在生产环境中平稳跑通,同时确保彼此的数据绝对不会发生交叉污染。
延伸阅读
先看同系列(隐私与合规):
- 后 Cookie 身份层:第三方 cookie / IDFA 退场后靠什么认人——本篇同意框架的上游身份背景。
- 地区广告政策与合规:同意信号过了之后,投放地规则与平台审核怎么收口。
跨系列相关:
- 程序化广告生态全景:先看全局,再定位「同意 / 隐私」这一层在投放链路里的位置。
- App 广告 SDK 深挖:端上同意管理、ATT、SKAdNetwork 的承载——本篇正是那篇里「协议基础另文展开」的一块。
- RTA Explained:同样是「收到信号后该怎么决策」,但 fail-open / fail-closed 的取舍正好和 TCF 相反。
- Header Bidding:Prebid.js vs Prebid Server:TC String 搭车的那条 OpenRTB 竞价链路。
再是规范与一手资料:
法律 / 政策
- IAB Europe. Transparency & Consent Framework Policies:法律 + 政策义务的权威来源。Appendix A 是 §4 里 11 个 Purpose、3 个 Special Purpose、3 个 Feature、2 个 Special Feature 的原文出处。
- CNIL. Délibération SAN-2023-009 du 15 juin 2023(Criteo):4000 万欧元罚单背后的裁决。对任何维护 TCF 相邻系统的人,都是一份简短而清醒的读物。
协议 / 技术规范
- IAB Tech Lab. Consent string and vendor list formats v2(Core String 字段直达):TC String 编码、段结构与版本历史表(v2.2 砍掉 purpose 3–6 的 LI;v2.3 让 Disclosed Vendors 强制)。
- IAB Tech Lab. Global Privacy Platform — Section Information:§7 里 GPP section 表的出处,外加 GPC subsection 和
CvsD首字符约定。
工具 / 实现
- IAB Tech Lab. iabgpp.com Encoder / Decoder:官方在线工具。贴进一条
regs.gpp或user.consent,把每个字段当 JSON 读。§3.2 那条 TC String 就是用它解码的。
相关 / 相邻
- IAB Tech Lab. OpenRTB 2.6 Specification:
user.consent、regs.gdpr、regs.gpp、regs.gpp_sid的权威位置。 - IAB Europe. Global Vendor List:在线 GVL JSON 端点。服务端同步从这里起步。
- Apple Developer. App Tracking Transparency:用来内化为什么 ATT 和 TCF 是两套独立的法域规则。
附录:术语表
- ATT(App Tracking Transparency):Apple 在 iOS 14.5+ 的隐私机制,控制 App 对 IDFA 的访问。独立于 TCF。
- CMP(Consent Management Platform):媒体部署的组件,弹出同意 UI、收集用户选择、生成 TC String,并暴露
__tcfapi/__gpp。 - Core String:TC String 里永远在场的主段,装 purpose 的 consent / LI、vendor consent 与元数据。
- CPRA(California Privacy Rights Act):CCPA 的升级版,2023 年起生效;对应 GPP section 8。
- Data Controller(数据控制者):GDPR 术语,决定处理目的与方式的实体。媒体通常是控制者。
- Data Processor(数据处理者):GDPR 术语,代表控制者处理数据的实体。vendor 通常是处理者。
- fail-closed / fail-open:信号缺失时的默认行为;closed = 拒绝,open = 允许。在 TCF 里几乎永远 fail-closed。
- GDPR(General Data Protection Regulation):2018 年起生效的欧盟法规;TCF 的法律源头。
- GPC(Global Privacy Control):浏览器发出的、表示「do not sell」的 W3C 草案信号。作为 subsection 桥接进 GPP。
- GPP(Global Privacy Platform):IAB Tech Lab 的全球合规信封,把 TCF、US Privacy、各州字符串一起携带。
- GVL(Global Vendor List):IAB Europe 的 vendor 注册表。分配 vendor ID,记录每家 vendor 的 purpose / feature / 法律依据声明。
- IAB(Interactive Advertising Bureau):行业组织。TCF 由 IAB Europe 发布,GPP 由 IAB Tech Lab 发布。
- Legitimate Interest(LI,正当利益):GDPR 合法性基础之一。用户默认 opt-in、可 opt-out。v2.2 砍掉了 purpose 3–6 的 LI。
- OpenRTB:IAB 的实时竞价协议。TCF 与 GPP 经
user.consent/regs.gdpr/regs.gpp传递。 - Publisher Restrictions:媒体对 vendor GVL purpose 声明的覆盖(禁用 / 强制 consent / 强制 LI)。
- Purpose:TCF 的 11 个数据处理目的之一,从「存 cookie」到「个性化广告」。
- Service-Specific Scope:TC String 有效性限定在当前媒体。如今唯一合法的 scope。
- Special Feature:需用户显式 opt-in 的高敏感能力:精确地理位置、主动设备指纹。
- Special Purpose:用户无法拒绝的 LI-only 目的:反作弊、广告 / 内容投递、保存隐私选择。
- Stack:TCF 定义的 purpose / special feature 分组,用于 CMP UI 展示。
- TC String:base64url 编码的 TCF 同意状态——那个物理传输格式。
- Vendor:在 IAB GVL 里注册、带唯一 vendor ID 的实体。