真正有效的做法是在Nginx access阶段提取多维客户端指纹(UA、JA3、HTTP头组合),结合共享内存白名单与动态拦截策略,毫秒级识别并差异化处置恶意爬虫,不触达后端。

静态资源请求本身不带业务逻辑,但恶意爬虫常伪装成正常用户高频刷取 CSS、JS、图片等文件,消耗带宽与 CDN 流量。单纯靠 IP 限流或 Referer 校验容易误伤或被绕过。真正有效的做法,是在请求抵达后端前,对客户端“行为指纹”做轻量级校验——比如 User-Agent 特征、请求头组合、TLS 握手特征(JA3),再结合访问频率动态决策。整个过程必须在 Nginx access 阶段完成,不触达后端,毫秒级响应。
提取多维客户端指纹
静态资源路径(如 /static/js/app.min.js)通常不带参数,无法靠 query string 做规则匹配,所以需从请求上下文中提取稳定、难伪造的标识:
- User-Agent 解析:提取语言栈、渲染引擎、是否含 headless、python-requests、curl 等关键词;注意区分真实浏览器(含 AppleWebKit、Gecko)与工具链(如 Go-http-client/2.0、playwright-chromium)
-
TLS 层 JA3 指纹:启用
ssl_preread on,在 stream 上下文捕获 Client Hello 原始数据,用lua-resty-ja3解析出 TLS 版本、密码套件、扩展顺序等五元组并生成 MD5。该指纹由客户端底层网络库决定,极难在应用层篡改 -
HTTP 头组合特征:例如同时存在
Accept: */*、Accept-Encoding: gzip, deflate、Connection: close且缺失Sec-Fetch-*系列头,大概率是非浏览器客户端
构建轻量级指纹白名单机制
不建议对所有静态资源做全量拦截,而是建立“可信指纹池”,只放行已知安全的客户端组合:
- 将主流浏览器(Chrome、Safari、Firefox)最新版 UA + 对应 JA3 哈希存入
lua_shared_dict(共享内存),避免 Redis 查询延迟 - 对首次出现的新指纹,不直接拦截,而是记录到 Redis 并打上
pending标签,观察其后续 5 分钟内是否触发异常行为(如 1 秒内请求 20+ 个 JS 文件) - 支持按域名或路径前缀配置白名单策略,例如
/assets/下允许更宽松的指纹范围,而/public/fonts/则强制要求带完整 Sec-Fetch-Dest 头
动态拦截与响应策略
匹配到可疑指纹后,不统一返回 403,而是分场景响应,提高对抗成本:
- 对低频试探性请求(单 IP 单指纹每分钟 ≤ 5 次),返回空 JS 文件(
content_by_lua直接输出/* empty */),让爬虫难以判断是否被识别 - 对高频批量请求(如同一 JA3 指纹 10 秒内请求 ≥ 15 个不同 .css 文件),写入 Redis 黑名单,并在后续请求中通过
access_by_lua_block查表拦截,返回 429 或静默断连 - 可叠加 IP + 指纹双因子限流:同一 JA3 下多个 IP 集中访问,或同一 IP 切换多个 JA3 快速试探,均触发增强限制
部署要点与避坑提示
该方案依赖 OpenResty 环境,关键配置不能遗漏:
- 确保
lua_need_request_body off(静态资源无需读取 body),避免性能损耗 - JA3 解析必须放在
stream { }块中,配合ssl_preread on,不能在 http 块内用$ssl_client_hello(Nginx 不提供该变量) - 共享字典大小要预估:每个指纹哈希约 32 字节,若缓存 10 万个常见指纹,
lua_shared_dict ja3_whitelist 5m即可满足 - 避免在
set_by_lua中做正则匹配或网络调用——该阶段要求极快,复杂逻辑一律移至access_by_lua_block











