Topics

自主开发符合 GDPR 要求的 Cookie 同意管理(CMP)的经验分享

  • column

虽然已经是前段时间的事了,但为了应对 GDPR/ePrivacy,我们自行开发了一款轻量的、仅供自有网站使用的 CMP(同意管理工具)。通常这类需求会直接引入 SaaS,但本文想谈谈“为什么要自研”以及“是怎么做的”。

首先是 Cookie 同意。对于面向海外提供服务的网站来说,这已经是无法回避的问题。欧盟的 GDPR 是最具代表性的法规,而在美国,各州的隐私法也逐年增多,“不经同意就运行统计或广告标签”已经不再是理所当然的做法。

在欧盟,根据 GDPR 和 ePrivacy 指令,在获得同意之前,不得设置 GA4 或 Google 广告等非必要 Cookie,也不得将浏览数据发送到外部。将“关闭”视为同意是不允许的,撤回同意比表示同意更麻烦的做法同样不合规。

该法规同样适用于欧盟域外的企业,只要其面向欧盟境内的人员提供服务,或追踪欧盟境内人员的行为即可。例如,提供英文版网站并向海外提供服务、投放再营销广告的日本网站,即使在欧盟没有营业地点,也可能适用该法规。

Google 方面也提出了要求。若要在欧盟地区的访问者中使用 Google 广告功能,必须支持 Consent Mode v2,如果未将同意状态正确告知 Google,衡量和再营销将无法正常运作。在国内,2023 年修订的《电気通信事業法》开始实施“外部发送规范”,要求公开哪些服务会接收哪些信息。

也就是说,我们需要一套同时满足以下三点的机制。

  1. 判定访问者所在地区,在欧盟等地区,在获得同意之前完全不运行任何标签
  2. 将同意状态正确传递给 Google Consent Mode,并提供随时可以撤回的途径
  3. 公开外部发送目的地及 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]   各タグ側の「追加の同意チェック」で最終ガード
  1. 地区判定 ― 在 Edge 上判定国家,并在 HTML 中添加标记
  2. 同意脚本 ― 根据标记和已保存的选择,决定横幅显示与 GTM 的启动
  3. 登记簿 ― 使用的外部服务和 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则判定为日本)的推测模式。

判定方式可按站点分别设置。无论设置如何,脚本都会先读取能够同步获得的信息。

  1. HTML 中嵌入的地区属性(如有 Edge 判定,则始终使用此项)
  2. Cookie 中记录的地区(适用于无法改写 HTML、由服务器通过 Cookie 传递判定结果的架构)
  3. 仅当以上 1 和 2 均不存在时,才按设置的方式(IP 判定 API 或时区)进行判定
  4. 若仍无法获取,则视为海外(**偏向安全侧**)

始终优先使用属性和 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) / 标记工程师 / 前端工程师 / 网页总监

查看此员工的文章

以稳健的团队体制和迅速的应对能力为傲

Liberogic 的资深团队积极推动项目,因此深受客户好评。
我们致力于合理配置项目经理与总监,确保整个项目顺利推进。 我们采用按需投入的方式,避免不必要的全员投入导致成本增加,合理分配资源、人尽其才。同时,我们在掌握业务内容、制作与提交报价方面的速度也广受好评。

※ 当社不积极开展 SES 类驻场业务,敬请谅解。

您可以使用 Slack、Teams、Redmine、Backlog、Asana、Jira、Notion、Google Workspace、Zoom、Webex 等几乎所有主流的项目管理工具和聊天工具。

在利用 SES 或离岸外包的大型项目中,您是否正在为技术难题或推进方式而烦恼?

案例分析