像真实隐私用户一样测试数百个网页的 Cookie 行为

2026-09-01 35 预计阅读时间: 1 分钟
来源: dropbox.tech AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

当网站数量从几个增长到几百个,人工检查 Cookie 很快就会失去可操作性。更可靠的做法是构建一个 Cookie 审计器:带着明确的隐私偏好访问页面,记录页面实际加载的 Cookie,再判断这些 Cookie 是否符合用户选择。

这个思路的重点不只是“页面有没有 Cookie”,而是验证一个完整的行为契约:用户拒绝某类 Cookie 后,页面是否仍然设置了对应 Cookie;用户允许必要 Cookie 时,关键页面是否还能正常工作;不同页面、子域名和加载路径是否保持一致。

审计器应该模拟什么

一个隐私意识明确的用户通常会有一组可执行的偏好,例如:

{
  "necessary": true,
  "analytics": false,
  "advertising": false,
  "functional": true
}

审计器可以为每组偏好启动独立的浏览器上下文,然后依次访问待测页面。每次访问至少记录以下信息:

  • 页面 URL 和最终跳转地址
  • 响应头中的 Set-Cookie
  • 浏览器上下文中最终存在的 Cookie
  • Cookie 的域名、路径、名称和属性
  • 页面加载期间发起的第三方请求
  • Cookie 与偏好类别之间的匹配结果

这样可以覆盖两类常见问题。第一类是服务端在响应头中提前写入 Cookie;第二类是 JavaScript、标签管理器或第三方脚本在页面加载后动态写入 Cookie。只检查浏览器开发者工具中的某一个时间点,很容易漏掉其中一类。

从单页检查扩展到数百个页面

规模化审计的关键是把“访问页面”和“判断结果”分开。访问器负责收集事实,规则引擎负责解释事实。这样,当 Cookie 分类规则变化时,不需要重新设计浏览器自动化流程。

可以为每个站点维护一份期望策略:

site: shop.example.test
pages:
  - /
  - /checkout
  - /account
allowed_cookies:
  necessary:
    - session_id
    - csrf_token
  functional:
    - locale
  analytics: []
  advertising: []

实际项目中,Cookie 名称通常需要支持正则表达式或前缀匹配,例如 ^session_。还应把第三方 Cookie 单独标记出来,因为同一个 Cookie 名称在不同域名下可能代表完全不同的行为。

批量运行时,可以把页面清单放在 CSV、数据库或配置仓库中,并为每次审计生成一个版本化报告。报告不应只给出“通过”或“失败”,还要保留发现证据,方便网站负责人定位是哪个页面、哪个域名、哪个脚本设置了不合规 Cookie。

一个可以改造的 Playwright 示例

下面是一个最小的 Python 示例。它假设页面已经提供了一个可自动化操作的同意管理界面,例如按钮带有 data-consent-analytics="reject" 属性。实际项目需要根据站点的 CMP 实现修改选择器。

安装依赖:

python -m pip install playwright
python -m playwright install chromium

保存为 cookie_audit.py 后运行:

import asyncio
import json
import sys
from pathlib import Path
from urllib.parse import urlparse

from playwright.async_api import async_playwright


async def audit_page(browser, url: str, preferences: dict) -> dict:
    context = await browser.new_context()
    page = await context.new_page()
    response_cookies = []

    async def capture_response(response):
        if "set-cookie" in response.headers:
            response_cookies.append({
                "url": response.url,
                "set_cookie": response.headers["set-cookie"],
            })

    page.on("response", capture_response)

    try:
        await page.goto(url, wait_until="networkidle", timeout=45_000)

        # 选择器是示例假设,请替换为目标站点 CMP 的实际控件。
        for category, allowed in preferences.items():
            action = "accept" if allowed else "reject"
            selector = f'[data-consent-{category}="{action}"]'
            button = page.locator(selector).first
            if await button.count():
                await button.click()

        await page.wait_for_timeout(2_000)
        cookies = await context.cookies()
        return {
            "url": url,
            "final_url": page.url,
            "preferences": preferences,
            "cookies": [
                {
                    "name": cookie["name"],
                    "domain": cookie["domain"],
                    "path": cookie["path"],
                    "secure": cookie["secure"],
                    "same_site": cookie["sameSite"],
                }
                for cookie in cookies
            ],
            "set_cookie_headers": response_cookies,
        }
    except Exception as exc:
        return {"url": url, "preferences": preferences, "error": str(exc)}
    finally:
        await context.close()


async def main():
    if len(sys.argv) != 2:
        raise SystemExit("usage: python cookie_audit.py pages.txt")

    urls = [line.strip() for line in Path(sys.argv[1]).read_text().splitlines() if line.strip()]
    preferences = {
        "analytics": False,
        "advertising": False,
        "functional": True,
    }

    async with async_playwright() as playwright:
        browser = await playwright.chromium.launch(headless=True)
        results = []
        for url in urls:
            results.append(await audit_page(browser, url, preferences))
        await browser.close()

    print(json.dumps(results, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    asyncio.run(main())

创建页面清单并执行:

printf '%s\n' \
  'https://shop.example.test/' \
  'https://shop.example.test/checkout' \
  'https://shop.example.test/account' > pages.txt

python cookie_audit.py pages.txt > cookie-report.json

这段代码负责采集结果,并没有硬编码“哪些 Cookie 一定合规”。生产环境中可以在报告生成阶段加入规则判断:如果 analyticsfalse,则拒绝名称匹配分析类规则的 Cookie;如果 Cookie 来自不在允许列表中的第三方域名,则标记为待审查,而不是直接假设它违规。

让结果足够可信

自动化浏览器只能告诉我们页面做了什么,不能自动解决 Cookie 分类本身的业务定义。审计系统需要明确几个边界:

  • “必要”应由隐私和产品负责人共同维护,不能因为某个脚本历史上一直存在就自动归入必要类别。
  • Cookie 名称会因版本、地区和登录状态变化,规则应支持域名、前缀和正则表达式。
  • 需要分别测试首次访问、已有同意记录、登录用户和清除站点数据后的场景。
  • 同一页面可能根据地理位置或实验分组加载不同脚本,测试结果应保存运行环境和时间。
  • Cookie 之外还应观察本地存储、像素请求和第三方网络请求,否则审计范围可能过窄。
  • 页面加载失败、CMP 没有出现、超时等情况应单独记录,不能简单算作“没有发现 Cookie”。

在数百个页面上运行时,可以按站点和偏好组合并发,但要限制并发度,避免对生产站点造成不必要压力。对不可重复的流程,例如下单或发送表单,应使用测试环境、只读路径或专门的测试账号。

落地时的检查清单

  1. 定义可机器判断的 Cookie 类别和允许列表。
  2. 为每组用户偏好创建隔离的浏览器上下文。
  3. 同时采集响应头 Cookie、浏览器 Cookie 和网络请求。
  4. 覆盖首页、登录后页面、关键转化页面以及随机抽样页面。
  5. 保存 URL、最终跳转、运行环境、时间和原始证据。
  6. 将“违规”“待人工确认”和“技术失败”分开统计。
  7. 在每次部署或 CMP 配置变更后自动运行审计。

这种审计器的价值不在于生成一份漂亮的 Cookie 清单,而在于验证用户偏好是否真正影响了页面行为。把它接入发布流程后,隐私控制就从一次性的人工检查变成了可以持续回归的工程测试。


相关推荐