虽然已经是前段时间的事了,但为了应对 GDPR/ePrivacy,我们自行开发了一款轻量的、仅供自有网站使用的 CMP(同意管理工具)。通常这类需求会直接引入 SaaS,但本文想谈谈“为什么要自研”以及“是怎么做的”。
首先是 Cookie 同意。对于面向海外提供服务的网站来说,这已经是无法回避的问题。欧盟的 GDPR 是最具代表性的法规,而在美国,各州的隐私法也逐年增多,“不经同意就运行统计或广告标签”已经不再是理所当然的做法。
在欧盟,根据 GDPR 和 ePrivacy 指令,在获得同意之前,不得设置 GA4 或 Google 广告等非必要 Cookie,也不得将浏览数据发送到外部。将“关闭”视为同意是不允许的,撤回同意比表示同意更麻烦的做法同样不合规。
该法规同样适用于欧盟域外的企业,只要其面向欧盟境内的人员提供服务,或追踪欧盟境内人员的行为即可。例如,提供英文版网站并向海外提供服务、投放再营销广告的日本网站,即使在欧盟没有营业地点,也可能适用该法规。
Google 方面也提出了要求。若要在欧盟地区的访问者中使用 Google 广告功能,必须支持 Consent Mode v2,如果未将同意状态正确告知 Google,衡量和再营销将无法正常运作。在国内,2023 年修订的《电気通信事業法》开始实施“外部发送规范”,要求公开哪些服务会接收哪些信息。
也就是说,我们需要一套同时满足以下三点的机制。
- 判定访问者所在地区,在欧盟等地区,在获得同意之前完全不运行任何标签
- 将同意状态正确传递给 Google Consent Mode,并提供随时可以撤回的途径
- 公开外部发送目的地及 Cookie 的清单(政策),并持续保证其与实际通信保持一致

为什么没有引入现成的 Cookie 同意管理工具
原因有以下三点。
- 标签冲突。
我们希望避免外部同意管理脚本与 GTM、广告标签以及 SPA 页面切换发生冲突,从而导致已同意用户的转化数据也丢失的情况。 - 可控对象明确。
我们使用的统计和广告工具几乎都属于 Google 体系(GTM / GA4 / Google Ads)。需要控制的标签已经确定,因此无需通用工具那样的复杂功能。 - 契合我们的优势。
前端实现和无障碍访问是我们的擅长领域。自行构建轻量且可审计的机制,只需提供所需功能,更为合适。
基本方针:日本采用通知,海外默认拒绝
我们确定了各地区的处理方式如下。
访问者 | 初始状态 | 界面 | Google 标签 |
|---|---|---|---|
来自日本 | 允许 | 无横幅 | 照常启动 |
来自日本以外地区 | 拒绝 | 页面底部显示横幅(全部允许 / 全部拒绝 / 详细设置)。选择后将显示浮动的设置按钮 | 仅在同意后启动 |
无法判定 | 拒绝 | 与海外相同 | 仅在同意后启动 |
存在 GPC 信号 | 仅拒绝广告 | 显示广告被拒绝的原因 | 停止广告用途 |
日本以遵循《外部传输规范》为基本,通过通知和公布来处理,不显示横幅。海外则将欧盟和美国统一设为初始状态为拒绝。美国多数州法律以退出(opt-out)为主,因此这一做法在某些方面可能略显严格,但为了简化运营,我们统一采用了更为安全的方案。
无法判定时同样视为拒绝,遵循的是同一逻辑。遇到拿不准的情况,就倾向于“不改动标签”。这是贯穿整体的设计理念,后文提到的“加载失败时的行为”也都遵循这一方向。
撤回途径应与同意“同样简单”
GDPR 要求撤回同意与作出同意同样简便。如果在横幅上两次点击即可同意,而撤回却只能从隐私政策页面深处进入,两者就不对等。
无论地区,打开设置对话框的“Cookie 设置”按钮都放在隐私政策页面的“更改与停止同意的方法”一节中。在海外模式下,除此之外,还会在用户通过横幅做出选择后,在页面左下角常驻一个悬浮设置按钮,使用户可以在任意页面更改或撤回同意。日本模式本来就不显示横幅,因此不显示悬浮按钮,仅保留隐私政策页面中的按钮。根据项目情况,也可以切换为“日本同样显示”或“不显示”。
撤回后,将删除登记簿中列出的 Google 相关 Cookie,并停止之后的数据发送。
结构:共 3 个部分
[Edge] Cloudflare Worker が request.cf.country を見て
<html data-lcmp-region="jp|overseas"> を HTML に注入
[HTML] <head> 先頭の同期スクリプト(loader.js)が
地域・保存済みの選択・GPC から gtag('consent','default') を確定
→ 日本 or 有効な同意あり → GTM を読む
→ それ以外 → GTM を読まず、バナー UI を遅延読み込み
[UI] 選択 → 検証・保存 → gtag('consent','update') → GTM を読み込む(何度呼ばれても挿入は1回だけ)
[GTM] 各タグ側の「追加の同意チェック」で最終ガード
- 地区判定 ― 在 Edge 上判定国家,并在 HTML 中添加标记
- 同意脚本 ― 根据标记和已保存的选择,决定横幅显示与 GTM 的启动
- 登记簿 ― 使用的外部服务和 Cookie 的清单。既是政策表的依据,也是撤回时删除对象的原始数据
① 地区判定在 Edge 上为 HTML 添加标记
国家判定使用 Cloudflare 的 request.cf.country 即可完成,但决定性的问题在于将其用在何处。
若采用由浏览器端调用判定 API 的方式(海外大型工具多采用此形式),在收到响应之前无法得知地区,日本访问者也会短暂处于“拒绝”状态。因此我们选择在 Edge 上改写 HTML,并在 <html><html> 中嵌入地区属性。仅需通过 HTMLRewriter 添加 data-lcmp-region="jp",脚本即可同步读取地区。
// dist/_worker.js/index.js(サイト側の post-build で生成)
import inner from './astro-entry.js';
import { wrapWorker } from './lg-region-wrap.js';
export default wrapWorker(inner);
本公司网站采用 Astro + @astrojs/cloudflare,因此遇到了一些难点。在适配器生成 _worker.js 的构成下,functions/ 目录会被忽略,而且预渲染页面也不会经过 Astro 的 middleware。最终,我们采用用包装器包裹构建后生成的 Worker 入口的方式进行注入。
不依据语言进行判定
我们也考虑过通过 <html lang>... 或 navigator.language 进行判断,但如果欧盟居民打开日语页面时被误判为 jp,就会在未获得同意的情况下触发标签,这属于风险方向的错误。
对于无法在服务器端插入判定的项目(例如使用 AWS 或虚拟主机、且 DNS 无法指向 Cloudflare 的架构),我们提供了一种通过 IP 地址向我们的判定端点发起查询的方式。这与常见同意管理工具的“供应商端判定 API”方式相同。该接口仅用于判定,不保存 IP 或国家代码,响应也只有jp或overseas两种值。若上述方式也不可用,作为最后手段,还提供基于终端时区(如为Asia/Tokyo则判定为日本)的推测模式。
判定方式可按站点分别设置。无论设置如何,脚本都会先读取能够同步获得的信息。
- HTML 中嵌入的地区属性(如有 Edge 判定,则始终使用此项)
- Cookie 中记录的地区(适用于无法改写 HTML、由服务器通过 Cookie 传递判定结果的架构)
- 仅当以上 1 和 2 均不存在时,才按设置的方式(IP 判定 API 或时区)进行判定
- 若仍无法获取,则视为海外(**偏向安全侧**)
始终优先使用属性和 Cookie,是为了之后迁移到 Edge 判定时只需更改设置即可完成。即使 IP 判定 API 超时,也只会归为海外,不会在未获得同意的情况下触发标签。
② 同意前“不读取”,而非“阻止”
Google 的 Consent Mode 有两种模式:Advanced 会先加载 GTM 并仅发送“拒绝”信号;Basic 则在获得同意之前完全不加载 GTM 本体。本项目采用 Basic。若让同意前加载 GTM 的只有同意脚本本身,整体结构会简单许多。
- 即使脚本加载失败,也不会启动非必要标签,因为没有读取GTM的主体。
- 不在HTML中直接写入GTM代码片段。也不采用将其作为GTM自定义HTML标签分发的方式(因为这样会在同意之前就读取GTM)。
- 同时在GTM端为每个标签设置“仅在获得分析/广告同意后才触发”的选项,实现双重防护。
“同意前通信为0次、Cookie为0个”已通过E2E测试在网络层面进行验证。
const FORBIDDEN_BEFORE_CONSENT = [
'**googletagmanager.com/**', '**google-analytics.com/**',
'**googleadservices.com/**', '**doubleclick.net/**', // …
];
test('T02: overseas first visit sends nothing', async ({ page }) => {
const hits: string[] = [];
await page.route('**/*', route => {
if (FORBIDDEN_BEFORE_CONSENT.some(p => matchGlob(p, route.request().url()))) hits.push(route.request().url());
return route.continue();
});
await page.goto('/');
await expect(page.getByRole('region', { name: /cookie/i })).toBeVisible();
expect(hits).toEqual([]); // 通信 0 件
expect(await page.context().cookies()).toEqual([]); // Cookie 0 件
});
撤回同意时,仅删除台账中列明的已知Cookie,不采用通配符方式批量清除。采用“删除已掌握的内容”的方式,因此如果台账中的Domain / Path与实际情况不符,删除就会失败。通过设计让台账错误时测试即失败,这对于保持隐私政策表与实际通信的一致性起到了关键作用。
③ GTM之外的嵌入内容(YouTube、地图、Web 字体)
仅停止GTM并不足够。YouTube和Google 地图的iframe,以及无法自托管的Web字体,不经过GTM,而是直接从页面加载,并在同意之前就设置Cookie。
为此,我们采用了在HTML端只需修改一处属性的方案。
<iframe data-lcmp-src="<https://www.youtube-nocookie.com/embed/ID>" data-lcmp-service="youtube" width="560" height="315"></iframe>
同意之前,显示根据台账数值生成的占位内容(“此内容将从YouTube加载,您的IP地址和浏览信息将被发送至提供方”+“仅本次显示 / 始终显示”按钮),获得许可后再加载真实内容。
我们没有采用监控DOM并在加载前自动改写的“自动拦截”方案。这种方式难以避免加载遗漏和显示错乱,而且该功能属于海外产品中不保证运行的功能定位。虽然需要手动修改iframe的一处属性,但可以确保稳定运行。
Webフォントも同じ仕組みで止められます。<link rel="stylesheet"> の href を data-lcmp-href に変えるだけで、同意前はフォントのCSSを読まず、許可後に読み込みます。ローダーは <head> で同期実行され、追加された要素を即時に処理するので、許可済みのときはパーサーが <link> を挿入した直後に href が戻り、フォントの読み込みタイミングは従来とほぼ変わりません。
<link rel="stylesheet" data-lcmp-href="<https://fonts.googleapis.com/css2?family=…>" data-lcmp-service="google-fonts">
不过,采用同意机制后,海外用户首次访问时字体不会被加载,页面将以系统字体显示。如果不希望这样,正确的做法是自托管:将字体文件放在服务器上,这样就不再有外部发送,相关争议也随之消失。
只需粘贴1行代码,8KB
前提是埋点和广告标签通过GTM运行。未使用GTM的网站无法直接导入(需要先将标签迁移到GTM)。
导入时,只需在 <head> 的最前面放置1段同步脚本。
<script src="<https://cdn.liberogic.jp/lcmp/><siteId>/loader.js"></script>
loader.js 经gzip压缩后约为8KB。横幅和对话框的UI位于单独的分块中,仅在需要时才延迟加载。
此外,GTM的 gtm.js 插入还提供了“LCP之后 + 数秒”的选项。启用该选项后,在插入前离开页面的访问者将不会被计入统计,这是一种权衡,但可以减少对Lighthouse分数的影响。
按站点发布与管理界面
配置(台账、GTM ID、文案、主题)在发布时打包进bundle,并通过CDN分发。运行时不会去获取配置JSON。这是为了同时满足“初始化同步且无通信”和“禁止从外部注入任意配置”,因此修改配置 = 重新发布并重新分发。每次发布都会保留版本记录,如有问题可以回退到上一版本。
发布产物除了 loader.js / ui.js 外,还附带可直接粘贴的代码片段、导入手册,以及已预先设置同意检查的GTM容器模板(JSON,用于导入)。这是为了减少导入到目标GTM时残留“未经同意检查的标签”这类事故。
这个管理后台让不懂开发的人也能完成这项发布工作。主要功能如下。
- 草稿与发布分离。
台账是各站点对外部发送数据的法律声明,因此编辑后不会立即生效。发布前会以日语变更列表展示与当前已发布设置的“差异”;如果修改了台账但政策修订日期未更新,则会发出警告。发布历史中可一键回退到上一版本。 - 常用服务目录。
可将 GA4、Google 广告、Clarity、YouTube、reCAPTCHA 等国内网站常用的服务添加到台账中,并附带名称、提供方、用途及 Cookie 的标准信息。分类仅为建议值,最终判断由人工完成。 - Cookie 自动扫描。
使用真实浏览器分别以“同意前”和“全部允许”两种状态打开目标页面,并将设置的 Cookie 及通信目标与台账进行比对。同意前已触发的标记为“严重”,台账中没有的标记为“注意”。可从检测结果中一键添加到台账。 - 政策表自动生成。
只需在政策页面中放置 。表格由 loader.js 根据内嵌的台账生成,因此政策表与实际控制对象不会出现偏差。 - 主题编辑。
可在查看实际预览的同时调整横幅、对话框和悬浮按钮的配色。文字颜色的对比度需达到 4.5:1,按钮边框等非文字元素需达到 3:1,系统会即时计算,不达标时发出警告。
总结
同意管理并非“显示横幅就结束”,只有地域判定、标签控制、政策公示和撤回途径这四项相互配合,才能真正成立。此次我们将这些功能集中到一个外部脚本和一份台账中。修改台账后,横幅说明、政策表以及撤回时的删除对象会同步变化,因此不存在不一致的地方。
通过投放广告获取收入的网站(如使用 AdSense 的网站)若要面向欧盟地区投放广告,必须使用 Google 认证的 CMP,因此不在本方案的适用范围内。但对于一般网站而言,本方案已足够使用。对于已确定所用标签、希望自行审计同意相关流程的网站,我认为相比直接引入现成工具,这会是更清晰透明的选择。
从 DTP 进入 Web 领域,不知不觉间,标记、前端、项目指导、无障碍样样精通的“技术仙人”。自 Liberogic 创业初期起就身兼多职,如今已是公司内的活字典般的存在。最近沉迷于用提示词提升效率的探索:“无障碍支持,能不能更多地依靠 AI 呢?”技术与思考仍在不断进化中。
二俣 歩
IAAP认证网页无障碍专家(WAS) / 标记工程师 / 前端工程师 / 网页总监