本文约 6,461 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:以为装了提示条就合规,结果被投诉的是「早于同意就埋点」
绝大多数外贸站的 Cookie 合规止步于”页面右下角弹了个小条”。这类提示条做了一件正确的事——告知,但恰恰漏掉了合规里最核心的一条:在用户做出选择之前,不能加载任何非必要的追踪脚本。
欧盟 GDPR + ePrivacy 的要求是「事前同意(prior consent)」:如果 GA4、Facebook Pixel、Hotjar 在用户点同意之前就已经向 Google 服务器发了请求并写下了 cookie,那么后端显示的”接受率 92%”在法律上是无效的——因为用户没有真正的拒绝选项,他还没来得及反应,数据就已经出去了。这正是 2022–2024 年间大批网站收到 CNIL(法国)、Garante(意大利)罚单的主要理由。
对国内面向欧盟 / 英国 / 加州用户的站点,这不是可选项。而且《个人信息保护法》对境内用户也有类似的”单独同意”要求。本文从技术落地角度,讲清一个真正达标的 Cookie 同意方案该怎么做。
原理分析:合规的四道硬门槛
| 要求 | 不达标的表现 | 技术判据 |
|---|---|---|
| 事前阻断 | 打开页面时 Network 面板已出现 googletagmanager.com 请求 | 未阻断时必然出现 |
| 拒绝与接受同等重要 | 只有「Accept」一个大按钮,拒绝要点两三次 | UI 层级对等 |
| 可随时撤回 | 关闭后再无入口 | 提供常驻重新打开入口 |
| 同意留痕 | 无法证明某用户何时同意了什么 | 有 consent log 记录 |
技术上「事前阻断」只有一种可靠做法:不要把 gtag/GTM 的 <script> 直接写进主题 head,而是交给同意插件按其状态动态注入;或者更彻底的方案——把 GA 脚本的 <script> 类型改成 text/plain 并加 data-cookiecategory="analytics",由同意插件在获得许可后把它改成 text/javascript 再 eval 执行。
CookieYes、Complianz、Borlabs Cookie、Real Cookie Banner 都支持这种”类别化管理”。Cookie Notice(由 Hu-manity.co 团队维护,安装量 100 万+)是轻量代表,但它的免费版在脚本阻断上能力有限,需要搭配手动改造。
第二件必须想清楚的是记录存哪。最简单能过关的做法是在 MySQL 里建一张 consent log 表,记录 consent_id / 匿名 IP 哈希 / 同意时间 / 同意的类别 / 使用的政策版本。不要记原始 IP(那本身就是个人信息),记 wp_hash() 之后的值即可。
保姆级实操
步骤 1:盘点站点上到底有哪些追踪脚本
动手之前先 inventory。用健康检查:
# 1) 抓自己首页,列出所有外域脚本/图片请求
curl -s -A "Mozilla/5.0" https://example.com/ \
| grep -oE '(script|iframe|img)[^>]*src="[^"]+"' \
| grep -oE 'https?://[^/"]+' | sort -u
# 2) 检查 Set-Cookie 的域与生命周期
curl -sI https://example.com/ | grep -i set-cookie
把结果按「必要 / 分析 / 营销 / 偏好」四类分好,这是后面所有配置的基础表。
步骤 2:安装并配置 CookieYes(免费版够大多数站点)
后台安装 CookieYes | GDPR Cookie Consent,激活后:
- Cookie Scanner:先跑一次自动扫描,它会枚举当前已存在的 cookie 并给出建议分类。注意扫描只能抓到”已发生”的 cookie,改完要重扫。
- Consent Banner → Layout:选 Classic 或 Box,
Show As一定选 Banner/Box 而不是 “Notification only”,后者没有按钮等于无效。 - Buttons:把 Reject 按钮打开。Accept 与 Reject 的样式必须一致(同色背景是可接受的,但 Reject 不能做成灰色小字)。
- Consent Expiry:建议 6–12 个月(很多监管机构认为超过 13 个月不合理)。
- Policy Version:每次改隐私政策后 bump 一次,插件会据此重新征求同意——这一步很多人忘了,导致老用户停留在旧政策的同意状态上。
步骤 3:把第三方脚本改造成可被阻断的形式
以 GA4 为例,原本的加载代码要改成这个形式(注意 type="text/plain" 与 data-cookiecategory):
<!-- Google Analytics 4:默认不执行,由同意插件解锁 -->
<script type="text/plain" data-cookiecategory="analytics" async
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script type="text/plain" data-cookiecategory="analytics">
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push( arguments ); }
gtag( 'js', new Date() );
gtag( 'config', 'G-XXXXXXXXXX', { anonymize_ip: true } );
</script>
对应的 GTM 容器也一样处理:
<script type="text/plain" data-cookiecategory="analytics">
(function(w,d,s,l,i){ w[l]=w[l]||[]; w[l].push({'gtm.start': new Date().getTime(), event:'gtm.js'});
var f=d.getElementsByTagName(s)[0], j=d.createElement(s), dl=l!='dataLayer'?'&l='+l:'';
j.async=true; j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl; f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');
</script>
步骤 4:记录同意日志(自己写一张 log 表最稳)
CookieYes / Complianz 都自带日志,但如果你想自己留一份便于审计:
/**
* 接收前端同意回调,写入自定义表。表结构见下方 SQL。
*/
add_action( 'wp_ajax_mf_log_consent', 'mf_log_consent' );
add_action( 'wp_ajax_nopriv_mf_log_consent', 'mf_log_consent' );
function mf_log_consent() {
check_ajax_referer( 'mf_consent', 'nonce' );
global $wpdb;
$cats = isset( $_POST['categories'] ) ? array_map( 'sanitize_key', (array) $_POST['categories'] ) : array();
$wpdb->insert(
$wpdb->prefix . 'consent_log',
array(
'ip_hash' => wp_hash( $_SERVER['REMOTE_ADDR'] . wp_salt() ),
'categories' => implode( ',', $cats ),
'policy_ver' => sanitize_text_field( $_POST['policy_ver'] ?? '1' ),
'created_at' => current_time( 'mysql', 1 ),
),
array( '%s', '%s', '%s', '%s' )
);
wp_send_json_success();
}
配套建表(用 dbDelta,放在插件激活钩子里):
function mf_create_consent_table() {
global $wpdb;
$table = $wpdb->prefix . 'consent_log';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
ip_hash varchar(64) NOT NULL,
categories varchar(255) NOT NULL,
policy_ver varchar(20) NOT NULL DEFAULT '1',
created_at datetime NOT NULL,
PRIMARY KEY (id),
KEY idx_created (created_at)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
步骤 5:提供「撤回同意」常驻入口
在页脚固定放一个链接,点击后 reopen banner:
// 页脚挂一个重新打开 Cookie 设置的入口
add_action( 'wp_footer', function() {
echo '<button type="button" class="mf-cookie-settings" data-cky-tag="settings-button">Cookie 设置</button>';
} );
CookieYes 的短代码版本也能直接用:[cky_preference]/[cky_policy]。
步骤 6:验证——最重要的一步
无痕模式打开站点,先不要点任何按钮,打开 DevTools Network 面板刷新:
# 期望输出为空:同意前不应有任何 analytics/googletagmanager 请求
curl -s https://example.com/ | grep -c 'googletagmanager.com'
再点 Accept,确认此时才出现 gtag/js 请求;点完全一致 Reject,确认没有任何第三方脚本请求;最后验证刷新后(已拒绝)依然不加载。
常见踩坑
- 同意前脚本已经跑起来了:八成是脚本被写在了主题 header.php 里,绕过了同意插件。必须走
type="text/plain"或插件的注入机制。 - 点了拒绝但 cookie 还在:旧 cookie 不会自动消失。需要在拒绝回调里显式删除
_ga/_fbp等:
js
document.cookie = '_ga=; Max-Age=0; path=/; domain=.' + location.hostname;
document.cookie = '_ga_XXXX=; Max-Age=0; path=/; domain=.' + location.hostname;
- 案例:某跨境电商站的 GA4 通过 Google Site Kit 安装(Site Kit 会在
wp_head里直接注入 snippet,不受同意插件管辖),导致无论用户点接受还是拒绝,GA 都在正常采集。德国客户做合规审查时抓到了这一条,要求限期整改并出具同意日志,而站点压根没有 consent log 功能。最后整个统计体系迁移到 GTM + 同意钩子方案才过关。教训:用 Site Kit 这类”一站式安装”的统计接入,必须额外确认它是否受同意插件管控。 - 只有 Accept 按钮:这是最常见的罚金事由(法国 CNIL 罚 Google 和 Facebook 的核心理由就是这个)。必须与 Reject 同等视觉权重。
- banner 被 z-index 压在下面:悬浮客服、登录弹窗的 z-index 经常是 99999,把 consent banner 盖住视为未告知。给 banner 容器 z-index 设为
2147483647。 - 对 EU 用户才弹,IP 库判断错误:IP 归属地判断存在误差,且很多用户用 VPN。稳妥做法是对所有访客展示(至少一次),只是对非 EU 用户不强制阻断,这属于合规实践里的保守选择。
- 同意日志记了原始 IP:日志系统本身就成了新的个人信息处理行为,反而增加义务。记 hash + 时间戳足够。
- iframe(YouTube、Google Maps)自动写入 cookie:嵌入 iframe 在点击前就会 set cookie。要么用「点击才加载」的 nocookie 预览图,要么明确写入隐私政策并在 banner 里声明为必要。
延伸阅读:Complianz 与 CookieYes 插件对比
总结
四句话的判断清单:①同意之前,Network 面板里不能出现任何第三方追踪域;②Accept 与 Reject 必须视觉对等;③任何页面都能重新打开设置修改选择;④同意要有日志,且日志本身不存敏感字段。 达到这四条,面对绝大多数审计基本站得住。工具选择上:预算有限、脚本简单用 CookieYes;WordPress + 服务器一体化的多区域方案用 Complianz(下一篇详解);不想引入插件、只需要管控少量脚本,用上面的 type="text/plain" 方案二十行代码也能达标。
合规配套内容:WordPress 隐私政策页面生成与修改
