爬虫换IP也能识别?浏览器指纹聚类实战

爬虫换 IP 是常态,封 IP 永远慢一步。本文用 10 个问答讲清浏览器指纹怎么采集、怎么把同一团伙的几百个 IP 聚成一类、Nginx 侧怎么落地拦截,以及误伤与合规怎么处理。

Q:爬虫一直换 IP,为什么光封 IP 不管用?

  • 住宅代理池和秒拨已经把「换 IP」做成按量付费的服务,一台设备一天换几百个出口地址是常规操作。
  • 你在维护一张越来越大的 IP 库,攻击者只要点一下切换。防御方是加法,攻击方是乘法,追不上。
  • 更麻烦的是误伤:一个企业出口 NAT 后面可能是几百个真实用户,封一个 IP 伤一片。
  • 浏览器指纹的价值在于,它看的是设备侧属性。IP 会变,显卡、字体、音频渲染不会跟着变。
  • 但要摆正预期:指纹不是「永久封死」的身份,它的作用是让攻击者的每个新 IP 都得重新过一遍设备成本。

Q:浏览器指纹和 TLS 指纹、User-Agent 有什么区别?

  • User-Agent:一行字符串,改起来零成本,只能做粗筛,不能做判据。
  • TLS 指纹(JA3/JA4):看握手时的套件顺序与扩展,改它要换客户端库,成本中等。缺点是同一版本 curl 或同一发行版浏览器会大量共用同一个指纹,只能缩小范围。
  • 浏览器指纹(应用层):需要真正把浏览器跑起来,采集渲染结果与设备参数。改它得打补丁或改源码,成本最高。
  • 三者是串联关系:UA 粗筛、TLS 中筛、浏览器指纹落到设备。只用其中任何一个都会漏,串起来才有意义。

Q:采集哪些字段?哪些稳,哪些会飘

  • 稳定集(建议 8 到 12 个):Canvas 渲染哈希、WebGL 的 vendor 与 renderer、字体列表、AudioContext 指纹、时区与语言、平台、hardwareConcurrency、deviceMemory、屏幕色深与像素比。
  • 易飘集(别进指纹):窗口尺寸、电池状态、网络类型、鼠标轨迹瞬时值、浏览器小版本号(自动升级就变)。
  • 屏幕分辨率先别用,接了外接显示器就变,笔记本用户一换就掉。
  • 字段不是越多越好。熵太高容易撞车误伤,同一型号的手机本来就长得一样。
  • 做法是把稳定字段按固定顺序拼成一个字符串,再做一次加盐哈希,只存哈希。

Q:指纹怎么采集才不容易被绕过

  • 关键不是 JS 写得多复杂,而是指纹必须带签名。不签名的 cookie 等于告诉对方「随便伪造」。
  • 流程:首次访问下发 fp_id,JS 算完指纹 POST 到 /_fp,服务端用 HMAC 签名后回写 cookie,后续请求带着它走。
  • 没有 JS 的客户端(curl、部分爬虫、老旧采集器)拿不到指纹字段。「无指纹」本身就是一个很强的信号,别浪费。
  • salt 要定期轮换,防的是别人拿你的哈希去反查设备特征。
// 前端最小采集(去掉 script 标签后内联到页面底部即可)
async function fpRaw() {
  const c = document.createElement('canvas');
  const g = c.getContext('2d');
  g.textBaseline = 'top';
  g.font = '14px Arial';
  g.fillStyle = '#f60';
  g.fillText('wafai-fp-2026', 2, 2);
  const cv = c.toDataURL().slice(-64);

  const gl = document.createElement('canvas').getContext('webgl');
  const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
  const gpu = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '';

  return [
    cv, gpu, navigator.hardwareConcurrency || 0,
    navigator.deviceMemory || 0, screen.colorDepth,
    Intl.DateTimeFormat().resolvedOptions().timeZone,
    navigator.language, navigator.platform
  ].join('|');
}

async function fpHash(s) {
  const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
  return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
}

// 上报,服务端再加盐签名后回写 cookie=fp
fetch('/_fp', {method: 'POST', headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({f: await fpHash(await fpRaw())})});

Q:几百个 IP 怎么聚成同一类

  • 不需要机器学习。指纹 + ASN + 时间窗三个维度就够用。
  • 三条判据:同一指纹 10 分钟内出现 3 个以上不同 IP;同一指纹 24 小时请求数 超过 5000;同一指纹跨 2 个以上 ASN。任一命中即标为可疑设备。
  • 团伙合并:把指纹、IP 集合、ASN 集合做成三列图,两组 IP 集合重叠超过 40% 就合并成一个团伙。
  • 时间窗要分开设:短窗抓压测和秒拨,长窗(7 天)抓慢速采集。只用一个窗,两类都抓不全。
  • 千万别用纯 IP 做聚类:NAT、校园网、运营商 CGNAT 会让几百个真人自然聚成一团。

Q:Nginx 侧怎么落地拦截

  • 核心思路:把限流 key 从 IP 换成指纹,IP 只做辅助判据。换 IP 不再等于换身份。
  • 没有指纹的请求不能一刀切拒绝,因为每个真人第一次访问时也还没有。只对「无指纹 + 高价值路径 + 非浏览器 UA」这个组合拒绝。
  • 限速桶给正常浏览器留足余量,命中后先返回 429 让前端退避,别直接 444 断连。
# 指纹优先,无指纹时退回 IP
map $cookie_fp $fp_key {
    ""      $binary_remote_addr;
    default $cookie_fp;
}

# 20m 约可存 16 万个 key;rate 按业务单页请求量调
limit_req_zone  $fp_key zone=fpzone:20m rate=30r/m;
limit_req_zone  $fp_key zone=fpburst:20m rate=120r/m;
limit_conn_zone $fp_key zone=fpconn:10m;
limit_req_status 429;
limit_conn_status 429;

log_format fp '$remote_addr - [$time_local] "$request" $status '
              'fp=$cookie_fp ua="$http_user_agent" rt=$request_time';

server {
    listen 443 ssl http2;
    server_name www.example.com;
    access_log /var/log/nginx/fp.log fp;

    # 无指纹 + 非浏览器 UA + 打接口,直接丢弃
    if ($cookie_fp = "") {
        set $no_fp 1;
    }
    if ($http_user_agent !~* "Mozilla/5.0") {
        set $no_fp "${no_fp}1";
    }
    if ($no_fp = "11") {
        return 444;
    }

    location /api/ {
        limit_req  zone=fpburst burst=60 nodelay;
        limit_conn fpconn 30;
        proxy_pass http://127.0.0.1:8080;
    }
}

Q:误伤主要来自哪些人

  • 企业出口与校园网:同型号电脑、同版本浏览器、同语言时区,指纹会撞车。建议指纹只做「加分项」,不单条定罪。
  • 隐私浏览器与无痕模式:Canvas 返回噪声,指纹每次都不一样,看起来像无限多个新设备。
  • 老旧设备:拿不到 WebGL,稳定字段直接少一半。
  • APP 内嵌 WebView 与小程序:环境不完整,指纹覆盖率天然低,别对它们套用 Web 的阈值。
  • 缓解办法:至少两个信号同时命中才处置;已登录用户按账号维度覆盖;对 429 提供自动重试而不是死页面。

Q:采集指纹会不会踩合规红线

  • 目的限定:只用于安全风控与反爬,不用于广告画像,不做跨站追踪。
  • 告知义务:隐私政策里写明「为安全目的采集设备特征并生成不可逆标识」,别藏在角落里。
  • 最小必要:只存加盐哈希,不存原始 Canvas 图像、不存完整字体列表,保留期建议 90 天。
  • 不做身份绑定:指纹不能当账号用,账号体系与设备标识分开存。
  • 面向欧盟用户要额外注意 ePrivacy 对终端设备信息的读取要求;国内则按个人信息保护法的最小必要与单独告知把握。

Q:怎么验收到底有没有用

  • 四个数:目标路径抓取页数、出口带宽、源站 CPU、被拦请求占比。四个数一起看,缺一个都可能自欺。
  • 误伤指标:429 与 403 里来自「已登录用户」和「已进入结算流程」的比例,红线 1%。
  • 指纹覆盖率:正常浏览器应高于 95%。掉到 90% 以下先查 CSP 和广告拦截插件,别怪攻击者。
  • 双向自测:无头浏览器改 UA、换代理 IP 后应仍被识别;真机(含 iOS 无痕、安卓厂商浏览器)跑一遍,确认不被拦。

Q:还有什么补充建议

  • 先跑「只记录不拦截」的影子模式 7 天,拿到真实分布再定阈值。直接上线拦截,第一天就会收到投诉。
  • 成本上,自研前端采集加服务端存储大约两到三个人周;厂商方案按请求计费,量大时报价差别很大,先算量再谈价。
  • 指纹会过期:浏览器升级、显卡驱动更新都会让部分字段变化。留好算法版本号,允许灰度切换。
  • 最后一层永远在业务侧:登录、下单、支付这些路径上,账号维度与风控规则比你前端写多少行 JS 都管用。