跨域匹配的本质不是同步数据。
它是在两个边界之间建立一条有条件的、可解释的、可撤销的对应关系。广告 Cookie Sync、OAuth 登录、支付回调、转化归因——这些看起来毫无关系的场景,底层都在处理同一件事:确认"我这里的这个对象"和"你那里的那个对象"是不是同一个东西。
这篇文章从四个场景展开,抽象出一个通用模型,以及什么情况下你根本不应该做匹配。
跨域匹配到底在匹配什么
先看"域"是什么。
这里的"域"不只是浏览器里的 domain。它可以是:
- 一个业务系统
- 一个组织
- 一个数据源
- 一个账号体系
- 一个设备空间
- 一个第三方平台
- 一个广告交易平台
- 一个支付渠道
- 一个分析系统
每个域都有自己的 ID 体系。
比如:
系统 A 中的用户:user_id = A123
系统 B 中的用户:user_id = B789
这两个 ID 本身没有任何天然关系。A123 不会因为长得朴素一点就自动等于 B789。工程里最危险的错误之一,就是看到两个字段都叫 user_id,就假装它们语义一样。
跨域匹配真正要建立的是一条映射关系:
A123 <-> B789
这条关系通常还不够,还要知道:
A123 <-> B789
source = cookie_sync
partner = some_exchange
created_at = 2026-05-21
expire_at = 2026-07-20
consent_scope = ...
confidence = ...
也就是说,匹配不是简单地“把两个 ID 存起来”。
它至少包含四层含义:
- 谁和谁匹配
- 为什么认为它们能匹配
- 这个匹配关系从哪里来
- 这个关系什么时候应该失效
缺少后面三点,前面的 ID 映射迟早会变成数据垃圾场。垃圾场的问题不是不能用,而是你用的时候永远不知道踩到的是数据还是雷。
为什么不能直接共享 ID
最直觉的想法是:既然两个系统都要识别用户,为什么不大家都用同一个 ID?
这想法很美好,像很多架构图一样美好。
现实里很难。
第一,系统边界不同。
一个广告交易平台不可能直接使用 DSP 内部用户 ID 作为自己的主键。一个支付平台也不可能把商户系统里的用户 ID 当成自己的用户 ID。
第二,权限边界不同。
你能识别用户,不代表你能把这个识别能力无条件交给别人。尤其是广告、金融、医疗、内容推荐这些领域,ID 本身就是敏感资产。
第三,生命周期不同。
A 系统里的 ID 可能长期稳定,B 系统里的 ID 可能会轮换、过期、清理、重置。强行统一 ID,最后会把两个系统的生命周期耦合在一起。
第四,合规边界不同。
有些 ID 在某个场景可以使用,在另一个场景不能使用。有些数据可以用于统计,不能用于个性化。有些用户同意了 A,不代表同意了 B。
所以更现实的做法不是共享一个全局 ID,而是建立一个受控的映射关系。
local_id + partner + scope -> external_id
这比"大家共用一个 ID"麻烦,但它保留了边界。
当然,统一 ID 并非在所有场景都是错的。同一个组织的内部微服务之间、同一信任域内的 SSO、同一数据平台内的用户画像——这些场景下统一 ID 是合理的,也是更简单的。问题不是统一 ID 本身,而是把统一 ID 的做法不假思索地推到跨信任域、跨组织、跨合规体系的场景里。判断标准不是"能不能统一",而是"这是不是一个信任域"。
这类架构选择看起来是自找麻烦,本质上是为了保留边界。边界平时碍事,出事故时就是防火墙。
广告投放里的 Cookie Sync 是一个很典型的场景
在广告投放链路里,Cookie Sync 是跨域匹配非常典型的一种实现。它不是竞价逻辑本身,但会影响后面的频控、受众匹配、重定向、归因和出价判断。
熟悉 OpenRTB 的话会很容易理解这个问题:SSP 或 Exchange 有自己的用户 ID,DSP 也有自己的用户 ID。两边都能识别“自己域名下的用户”,但不能直接读取对方域名下的 cookie。
假设用户访问一个媒体网站。页面里加载了广告请求,SSP 或 Exchange 能在自己的域名下识别这个浏览器,比如:
exchange.com -> ex_uid = E123
DSP 也可能在自己的域名下识别过这个浏览器,比如:
dsp.com -> dsp_uid = D789
问题是浏览器的 cookie 按域隔离:
exchange.com 不能直接读取 dsp.com 的 cookie
dsp.com 也不能直接读取 exchange.com 的 cookie
所以双方不能直接说:
E123 就是 D789
它们需要通过浏览器跳转、像素请求、302 redirect 之类的方式,让浏览器分别访问双方的域名。这样每一方都只能读取自己域名下的 cookie,但通过参数传递,最终建立映射表:
exchange_user_id = E123
dsp_user_id = D789
之后 Exchange 给 DSP 发 OpenRTB bid request 时,就可以在合适的情况下带上 buyer 侧 ID,比如:
{
"user": {
"id": "E123",
"buyeruid": "D789"
}
}
这里有一个很重要的点:
user.id 通常是供应侧、Exchange 或 SSP 的用户 ID。
buyeruid 才更接近 DSP 自己的用户 ID。
如果 DSP 把 user.id 直接当成自己的用户 ID 用,那就是典型的字段名驱动开发。字段名看着像,语义完全不是一回事。后面频控、归因、人群匹配全都会乱。
不要先写 match table,再说后面可以删。
在隐私相关系统里,"先污染,后治理"通常会把工程债升级成合规风险。技术债影响的是维护成本,合规风险影响的是业务能否继续使用这套数据。
账号绑定也是跨域匹配
把视角从广告拿出来,账号绑定其实也是同一个问题。
比如一个用户用 GitHub 登录你的系统:
你的系统 user_id = U1001
GitHub user_id = G7788
用户点击“使用 GitHub 登录”之后,你通过 OAuth 拿到 GitHub 的用户身份,然后在本地建立绑定关系:
provider = github
provider_user_id = G7788
local_user_id = U1001
这就是跨域匹配。
它和 Cookie Sync 的区别只是流程更“文明”一点:
- 用户显式点击登录
- OAuth 提供标准授权流程
- provider 返回身份信息
- 本地系统落绑定关系
但本质没变。
你不能把 GitHub 的用户 ID 当成本地用户 ID。
你也不能假设同一个邮箱就一定是同一个人。
你更不能在用户没有确认的情况下,把多个身份来源随便合并。
账号绑定里最容易出问题的是“合并策略”。
比如:
Google 登录邮箱 = a@example.com
GitHub 登录邮箱 = a@example.com
能不能自动合并?
默认不建议。
因为邮箱是否已验证、provider 是否可信、用户是否实际控制这个邮箱、历史账号里是否已有数据,这些都会影响安全边界。账号体系不是做字符串 join。把两个账号合错,比不合并严重得多。
不合并只是用户体验差。
合错了就是数据越权。
这也是跨域匹配里非常关键的一条原则:
匹配关系一旦会影响权限、资产、身份,就必须保守。
广告里的匹配错了,可能导致频控和投放效果变差。
账号里的匹配错了,可能直接把一个人的数据给另一个人看。
两个都糟糕,但糟糕程度不是一个量级。
支付回调也是跨域匹配
支付系统里也有类似问题。
你的订单系统里有:
order_id = O123
user_id = U1001
支付渠道里可能有:
payment_id = P999
transaction_id = T888
你创建支付单时,需要把本地订单和支付渠道订单关联起来:
local_order_id = O123
provider_payment_id = P999
支付成功后,支付平台通过 webhook 回调你:
{
"payment_id": "P999",
"status": "success",
"amount": 10000
}
你不能看到 P999 就直接给用户加余额。你要做的是:
- 找到本地映射关系
- 校验签名
- 校验金额
- 校验币种
- 校验订单状态
- 做幂等处理
- 再更新本地订单
这里的匹配不是用户匹配,而是订单匹配。
但底层模式还是一样:
external_id -> local_id
并且这类场景比广告更不能随便。
广告系统里一个 ID 匹配错了,可能导致投放和归因数据变脏。支付系统里一个 ID 匹配错了,影响的是资金、对账和审计,风险级别完全不同。
转化归因里的匹配更微妙
转化归因也是跨域匹配。
广告曝光发生在一个系统:
impression_id = I123
user_id = U1
campaign_id = C1
用户后来在广告主网站转化:
conversion_id = CV999
order_id = O888
归因系统要判断:
这次 conversion 是否应该归因到之前某次 impression/click?
这里的匹配不再只是 ID 等值匹配。它还会涉及:
- 用户 ID 是否一致
- 点击 ID 是否存在
- 时间窗口是否满足
- campaign 是否一致
- 设备是否一致
- 是否跨设备
- 是否 view-through
- 是否 click-through
- 是否有多个广告触点
- 归因模型是 last click 还是其他规则
这说明跨域匹配不是只有一种强度。
可以粗略分成三类:
| 类型 | 例子 | 特点 |
|---|---|---|
| 确定性匹配 | OAuth 账号绑定、支付订单映射 | 有明确授权或强校验 |
| 半确定性匹配 | Cookie Sync、点击 ID 归因 | 依赖 ID、时间窗口、上下文 |
| 概率性匹配 | 跨设备推断、相似行为匹配 | 有置信度,不能当事实用 |
概率性匹配尤其要小心。跨设备推断、相似行为匹配这类手段,本质上是"猜测",不是"确认"。猜测在广告投放和增长分析里有用,但不能被下游当作事实来消费——比如不能因为概率匹配认为两个设备属于同一用户,就把一方的个人数据展示给另一方。概率性匹配的结果,下游每多传一层,置信度就应该衰减一层。最危险的做法是把置信度 0.6 的匹配当成确定性匹配塞进同一张表、同一个字段,让后续所有消费者都默认它是事实。
我个人更倾向于把确定性匹配和概率性匹配在数据结构上明确分开。不要都塞进一个 matched_user_id 字段里。短期省字段,长期毁排查。
一个比较清楚的结构应该包含:
match_type = deterministic / probabilistic
match_source = oauth / cookie_sync / click_id / device_graph
confidence = 1.0 / 0.82 / ...
matched_at = ...
expires_at = ...
工程上最怕的是"字段看起来简单,语义被塞爆"。
一个字段承担五种含义,最后所有人都说"历史原因"。这种历史原因通常意味着当年的数据建模没有把来源、强度和生命周期表达清楚。
跨域匹配的核心模型
不管场景怎么变化,跨域匹配都可以抽象成这个模型:
subject in domain A
|
| evidence / protocol / consent / verification
v
subject in domain B
落到数据结构上,大概是:
CREATE TABLE identity_match (
domain_a VARCHAR(64) NOT NULL,
id_a VARCHAR(256) NOT NULL,
domain_b VARCHAR(64) NOT NULL,
id_b VARCHAR(256) NOT NULL,
match_type VARCHAR(32) NOT NULL,
match_source VARCHAR(64) NOT NULL,
confidence DECIMAL(5,4) NULL,
consent_scope VARCHAR(128) NULL,
evidence_id VARCHAR(256) NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NULL,
PRIMARY KEY (domain_a, id_a, domain_b, id_b)
);
这不是说所有系统都应该建这么一张通用表。
通用 identity graph 很容易变成过度设计,尤其是在业务还没复杂到那个程度时。什么时候值得考虑升级?几个信号:你发现自己同时在维护三套以上的专用匹配表;跨场景的匹配查询开始需要多表 join;或者不同业务线开始各自建匹配逻辑、彼此不一致。出现这些信号之前,专用映射表就够了。
但这个模型能提醒自己:匹配关系不是一个裸 ID,它至少有来源、范围、强度和生命周期。
在具体系统里,可以根据场景缩小:
广告匹配:
partner_id
partner_user_id
dsp_user_id
source
expires_at
OAuth 绑定:
provider
provider_user_id
local_user_id
verified_email
bound_at
支付订单:
provider
provider_payment_id
local_order_id
status
signature_verified
转化归因:
conversion_id
touchpoint_id
match_type
attribution_window
attributed_at
重点不是表怎么设计得漂亮,而是不要丢掉语义。
什么时候不应该匹配
不是所有能匹配的东西都应该匹配。
至少有几种情况要明确拒绝:
1. 没有合法授权或用户同意
涉及用户身份、行为、广告、设备标识时,如果没有对应授权,不应该做匹配。
尤其是广告场景,不能因为技术上可以发 sync pixel,就默认可以同步用户。Cookie Sync 本质上处理的是在线标识符,它不应该绕过 privacy gate。
2. 匹配结果会扩大权限
如果一次匹配会让用户看到更多数据、获得更多权限、访问更多资产,那必须走更强校验。
比如账号自动合并、企业账号绑定、支付账户绑定,都不能只靠邮箱或者昵称这种弱信号。
3. ID 来源不可信
第三方传来的 ID,如果没有签名、没有来源校验、没有白名单,不要直接写入核心映射表。
开放式回调、开放式 redirect、随便接受 partner 参数,这些都是事故入口。第三方 ID 进入核心映射表之前,必须先通过来源校验和权限边界。
4. 无法解释匹配依据
如果线上出了问题,你回答不了:
这个 A 为什么会匹配到这个 B?
什么时候匹配的?
谁触发的?
依据是什么?
还能不能撤销?
那这套匹配系统就是不可运维的。
不可运维的系统,迟早会把"偶发问题"升级成"全员排查"。
匹配系统的几个工程原则
第一,ID 要带命名空间
不要只存:
user_id = 123
要知道它是谁的 ID:
domain = exchange_a
user_id = 123
否则两个系统都生成了 123,你就获得了一次免费的数据串线体验。
第二,映射关系要有方向感
有些映射是对称的:
A <-> B
有些映射不是:
external_id -> local_id
比如支付回调里,外部 payment ID 只能映射到本地订单,不代表本地订单可以无条件反推出所有外部状态。
广告里也类似。Exchange 的 user.id 映射到 DSP user ID,不代表这个 ID 可以被拿去其他 partner 场景复用。
第三,匹配要有生命周期
Cookie 会过期,token 会过期,consent 会变化,设备会重置,用户会解绑账号。
所以映射关系也应该过期。
永不过期的匹配表很诱人,因为省事。但省事通常只是把问题存在未来。未来不会感谢你,它只会把那张表膨胀到没人敢动。
第四,匹配要可撤销
用户解绑 GitHub,映射关系要删除或失效。
用户撤回 consent,广告 ID 映射要停止使用。
支付订单关闭,不能继续接受后续成功回调直接改状态。
跨域匹配不是只负责建立关系,还要负责解除关系。
第五,主链路不能依赖慢匹配
在 RTB 这种低延迟场景里,bid request 进来之后再跨库、跨区、跨服务慢慢找 ID,不现实。
匹配关系应该提前建立,主链路只做低延迟查询。
这点不只适用于广告。支付回调、登录鉴权、权限判断也是一样。核心链路里每增加一个远程依赖,系统就多一个不可用来源。低延迟主链路应该尽量依赖本地索引、缓存或已预计算的映射结果。
第六,必须有观测能力
至少要知道:
match request 数量
match 成功率
match 失败原因
privacy block 比例
映射写入成功率
映射查询命中率
过期清理数量
partner 维度异常
没有这些指标,跨域匹配系统出问题时只能靠猜。靠猜不是工程方法。
我现在对跨域匹配的理解
这个领域不是停滞的。隐私增强匹配技术——Private Set Intersection (PSI)、Google PAIR、数据洁净室(data clean room)——正在改变匹配的技术手段,从"把 ID 传来传去"转向"在各自域内计算交集而不暴露原始 ID"。这些方案不是银弹,但它们指向同一个方向:匹配和隐私不是对立的,前提是系统设计必须正视授权、边界和可审计性。
跨域匹配本质上不是"同步数据"。
它更像是在两个边界之间建立一条有条件的、可解释的、可撤销的对应关系。
这里面最重要的不是 ID,而是边界。
没有边界,匹配就会变成数据混用。
没有来源,匹配就会变成黑盒。
没有过期,匹配就会变成污染。
没有合规,匹配就会变成风险。
没有观测,匹配就会变成玄学。
Cookie Sync 只是这个问题在广告系统里的一个典型表现。它把浏览器同源策略、广告交易、用户识别、隐私合规、低延迟系统和数据建模揉在了一起,所以看起来复杂。
但把它拆开之后,它和很多工程问题是相通的:
我是谁?
你是谁?
我们怎么证明这两个身份有关?
这个关系能用在哪里?
什么时候失效?
出了问题怎么解释?
能回答这些问题,才算真的理解了跨域匹配。
否则只是会拼几个 redirect URL。那只能说明你知道流程形态,还不代表理解了这套机制的边界和风险。
这篇文章讨论的是工程设计原则,不替代具体法域下的法律、隐私或合规判断。涉及用户身份、广告标识、支付资产和跨组织数据合作时,最终方案仍然需要结合业务场景、合同约束和当地法规单独评估。
参考资料
- IAB Tech Lab, OpenRTB 2.6 Specification.
- IAB Tech Lab, Publisher Advertiser Identity Reconciliation.
- Google Ad Manager Help, About PAIR.
- IETF Datatracker, Private set intersection based on ECDH.