
瑞数这四个字做过 js逆向的朋友应该都不陌生。它不是普通 WAF而是一套动态安全方案每次加载页面它都会塞给你一段高度混淆的 JS让你在浏览器里“现场做作业”算出一个带时效的动态 cookie后续请求必须带上它校验通过才放行。说句实话早期我碰瑞数也是硬着头皮啃混淆直到做案例五这个项目才把整套流程沉淀成一套通杀模版——既能用 playwright 过瑞数也能围绕瑞数 6代补环境做排查。这篇文章就把这套模板的完整思路、关键代码和踩坑记录都拉出来聊聊适合正在跟瑞数死磕的爬虫工程师和安全测试同学。先说清楚边界我这里讲的“通杀”不是一份代码永久有效而是一套可以快速适配不同版本瑞数的流程。瑞数并不傻它隔一段时间就会升级混淆和检测项所以真正能通杀的是你的思路、模块划分和调试方法而不是某一个 AST 还原结果。下面我会从瑞数的防护原理开始拆再给出一套可落地的模板设计最后分享几个最容易被忽略的坑。1. 瑞数到底在防什么先搞懂对手1.1 瑞数不是普通 WAF很多人第一次遇到瑞数保护的网站第一反应是“这是个强 WAF封 IP 挺厉害”。其实它的核心能力不在封 IP而在动态防护。瑞数信息的产品经常会出现在金融、政务、电商、酒店等网站的防护体系里它的思路是让每一次正常用户访问都生成一个“动态令牌”这个令牌由前端 JS 结合浏览器环境、鼠标行为、时间偏差、页面 DOM 状态等因素算出来服务器端再校验。拿典型的访问流程来说你直接请求目标页面服务器返回的不是正常 HTML而是一段或多段加密过的 JS 代码。浏览器自动执行这段 JS期间会疯狂读取navigator、canvas、WebGL、AudioContext、performance等环境信息。JS 执行完会在当前域名下种下一个动态 cookie同时页面可能自动刷新或跳转到真实业务页。后续请求如果没带这个 cookie或者 cookie 里的环境指纹对不上服务器就会返回“验证失败”、跳转或者无限重试。所以它防的不是“看不懂 JS”而是“非浏览器环境 非真实指纹”。你要过它要么让真实浏览器自己去执行 JS要么花大力气在 Node 等环境里把浏览器指纹“补”齐。这两条路就是整篇文章的主线。1.2 为什么传统硬刚脚本走不通早期做 js逆向大家习惯把网站里的核心加密函数抠出来放到 Node 里调用再用 Python 去跑结果。这一套在多数加密参数上行得通但碰到瑞数就非常难受原因有三个第一代码极度过混淆。瑞数 6代的核心混淆层已经做得非常复杂变量名全部编码化控制流拍平字符串动态拼接还有多层数组位移和自定义编码。你就算用 AST 初步还原光看懂主流程就要花不少时间而且每次版本升级可能换个包装方式前面还原的成果就废了。第二环境检测面太广。瑞数校验的不只是“你算出什么数字”还包括“你在什么环境里算”。它会遍历浏览器的属性特征检查document对象里的节点是否真实存在监听各种计时器甚至通过Function.prototype.toString检查某些函数是不是原生方法。这些在纯 Node 环境里全是漏洞稍微少一个属性就进死循环或者算错结果。第三动态 cookie 存在时效和绑定关系。cookie 的值会绑定来源 IP、UA、域名、时间戳你即使计算出来放到另一个环境里发请求也可能因为 UA 不一致被判定为伪造。所以现在的主流方案早就不是“把 JS 抠出来仿写”而是分成两条路用 playwright 这类自动化浏览器去“借力”或者用补环境工具去“造假”。通杀模板要做的就是把这两条路整合成一套可复用流程。1.3 通杀模板的目标和边界我给这套模板定下的目标是输入一个目标 URL自动识别瑞数版本自动选择执行方式最终输出一个可直接复用的动态 cookie并验证该 cookie 是否能成功访问业务接口。听起来很美好但边界也要说清楚通杀模板不是“拿到任何网站都能秒过”。它首先需要你确认目标网站在保护范围内且你拥有合法授权。模板会提供一个默认的“环境指纹”集合和若干兜底策略但遇到瑞数小版本更新时可能需要做少量配置调整。模板的侧重点是 5代和 6代因为这两个版本是现在的主流。4代及更老版本因为样本少了很多特征不一致我会简单提及但不会展开。把边界划清楚后面设计模块才不会走偏。2. 通杀模板的设计思路2.1 能通杀的不是某一段代码而是一套流程我最初也试图写一个“万能 JS”后来发现这条路走不通。瑞数每次请求产生的 JS 内容都不同如果你盯着代码写还原逻辑写出来的东西最多撑两个礼拜。真正稳定的做法是把流程固定住让每一步都“可替换”。整套流程可以这样描述识别目标指纹 - 选择执行容器 - 等待动态计算 - 抓取生成的cookie - 校验cookie可用性 - 接入业务会话关键点在于识别目标指纹决定了你后面的路径。如果目标网站的响应里带有明显的瑞数 JS 特征比如存在类似$cz、_$等常见关键变量以及大量十六进制字符串的script标签那基本可以判定是瑞数保护。继续往下看如果检测到window._ts或某些 WebAssembly 特征有可能是 6代玩法如果只有传统 OB 混淆大概率是 5代或更早。版本识别之后选择哪种执行方式这就要看你的场景了如果并发要求高、需要大规模采集那首选还是补环境 高并发执行。如果只是低频访问、业务接口不多playwright 过瑞数是最快最稳的方案毕竟真实浏览器帮你把环境、指纹全解决了。如果遇到 6代且补环境工程量太大就干脆把所有页面加载任务都交给 playwright把 cookie 取回来喂给 requests 也是一种通杀思路。2.2 三个可选方案对比我整理了一个表格对日常最常用的三种“过瑞数”方案做对比方案开发成本过检率资源占用稳定性适用场景纯 JS 补环境高中低中版本更新容易挂高并发、批量采集、需要抠核心算法Playwright 动态执行低高高高跟随浏览器升级低频请求、验证流程复杂、快速交付混合方案Playwright 取 cookie requests 发业务请求低高中高业务请求轻量需要快速接入现有 Python 采集框架我自己的习惯是优先走混合方案。理由很简单瑞数最烧脑的部分就是“环境计算”playwright 把这一步交给了真实浏览器而拿到动态 cookie 之后真正采集数据还是用 requests 或 httpx这样既能保持代码清爽又不需要维护一整套 Node 补环境工程。2.3 模板的功能模块拆解把模板拆成四个模块职责清晰方便单独替换版本识别模块负责分析响应内容判断目标是否被瑞数保护、大版本是几代、当前页面的验证方式是什么。这一模块不需要太深能区分 4/5/6 代就够了因为后面执行容器的选择主要依赖它。执行容器模块负责产生动态 cookie。内部封装了两种实现playwright 浏览器容器和手动补环境容器。模板默认会先尝试 playwright失败再切补环境也可以在配置里手动指定。Cookie 抓取与校验模块从浏览器上下文里拿到所有 cookie提取动态校验字段然后主动发一次带 cookie 的请求去目标接口用状态码和响应内容判断这个 cookie 是否真的能用。会话接入模块把验证通过的 cookie 转成 requests.Session 的 cookie或者导出成 header 字符串方便其他程序直接复用同时在检测到 cookie 过期时自动重新获取。这四个模块基本就是整个通杀模板的骨架。后面我会挑核心环节的实现细节展开尤其是 playwright 部分和 6 代补环境部分。3. 核心环节实现3.1 用 Playwright 过瑞数的标准姿势如果你时间紧不想研究 JSplaywright 过瑞数是最省力的方案。它的核心思想很简单让真实 Chromium 浏览器去访问目标页面等瑞数 JS 自己执行完偷偷从浏览器上下文里把动态 cookie 拿出来。这里我先给出一段能直接跑的 Python 示例后面再讲为什么这些参数很重要。import time from playwright.sync_api import sync_playwright def get_ruishu_cookie(target_url: str, cookie_names: list) - dict: with sync_playwright() as p: browser p.chromium.launch( headlessFalse, # 建议先有头调试稳定后再改 headless args[ --disable-blink-featuresAutomationControlled, --disable-gpu, --no-sandbox, ] ) context browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, viewport{width: 1920, height: 1080}, localezh-CN, timezone_idAsia/Shanghai, ) page context.new_page() # 减少非必要资源加载提升速度 page.route(**/*.{png,jpg,jpeg,gif,webp,woff,ttf,svg}, lambda route: route.abort()) page.goto(target_url, wait_untildomcontentloaded, timeout60000) # 等待动态cookie生成轮询检测 result {} for _ in range(20): cookies context.cookies() cookie_dict {c[name]: c[value] for c in cookies} if all(name in cookie_dict for name in cookie_names): result cookie_dict break time.sleep(0.5) browser.close() return result这段代码有几点很重要args里的--disable-blink-featuresAutomationControlled会关掉navigator.webdriver标志这个标志是很多检测脚本会检查的尤其是瑞数 6代。headless 模式设置新版本 Chromium 的 headless 模式已经越来越接近有头模式但瑞数对 headless 仍然有很强的探测能力。如果你的目标网站校验严格建议开发阶段先用headlessFalse调通流程稳定后再把它改成 headless同时观察是否掉线。page.route拦截资源瑞数页面不需要图片、字体这些资源直接取消加载能明显减少等待时间。等待方式不要写死动态 cookie 的生成需要 JS 执行完毕可能不到一秒也可能要好几秒。轮询 cookie 比固定sleep(3)要稳健得多。如果你需要更高频地获取 cookie那不建议每次重新启动浏览器。可以把浏览器实例打成常驻服务只创建新 context 和页面每取一次 cookie 就关闭 context这样能把单次耗时压缩到 2 秒以内。3.2 瑞数 6代补环境的关键点Playwright 虽好但在高并发场景下一个浏览器实例占用的内存和 CPU 都很高。很多团队仍然希望用 Node 补环境的方式去模拟浏览器直接在内存里把瑞数的动态 cookie 算出来。这套路就是“瑞数 6代补环境”。我见过不少同学一上来就无脑堆属性把网上流传的“补环境框架”拿过来就往里套结果还是不通过。原因很简单补环境不是“属性多”就行而是要对齐检测逻辑。瑞数 6代强的不是某一个单一检测点而是组合拳。这里我挑几个最关键的地方讲3.2.1 基础 window / document / navigator 代理在 Node 里运行瑞数 JS 时一进门就会读取window、document、navigator、location、history。这些必须全部存在而且不能是普通对象最好用Proxy做一层 getter/setter 拦截因为瑞数会检查属性描述符甚至尝试调用Object.getOwnPropertyDescriptor。const fakeWindow new Proxy({}, { get(target, prop, receiver) { switch (prop) { case navigator: return fakeNavigator; case document: return fakeDocument; case location: return fakeLocation; // 其他属性按需补齐 } if (prop in target) return target[prop]; return undefined; }, set(target, prop, value) { target[prop] value; return true; } });要注意瑞数会判断window.window window、window.top window、window.self window这类循环引用所以代理对象里的window、self、top、parent都要指向自身。3.2.2 Canvas / WebGL / AudioContext 指纹这部分是重头戏。瑞数会读取canvas.toDataURL()、canvas.getContext(2d)的某些渲染结果也会通过WebGLRenderingContext.getParameter()获取显卡信息还可能用AudioContext计算音频指纹。在 Node 环境里你没法真的去渲染 Canvas怎么办两种选择如果你做过浏览器指纹采集可以把目标网站真实返回的指纹结果存下来然后在 Node 里“伪造返回值”。比如把canvas.toDataURL直接写成固定字符串。更稳的做法是引入一个轻量级 Canvas 实现比如node-canvas但补环境和实际浏览器仍有差异需要反复调。我的经验是先抓一份真实浏览器跑出来的指纹值再代码里把这些值写死同时保证同一环境多次执行结果一致。瑞数如果对同一环境两次算出不同结果会直接判定异常。3.2.3 Function.prototype.toString 和原生函数特征瑞数 6代非常爱检查“某个函数是不是原生函数”。正常浏览器环境下Function.prototype.toString.call(Array.isArray)返回的是function isArray() { [native code] }。如果你在 Node 里用 JS 模拟了某个 API那 toString 就会暴露成自定义代码。所以补环境框架里都会把原生方法和模拟对象包一层在 toString 上动手脚。Function.prototype.toString function () { if (this Array.isArray) return function isArray() { [native code] }; return originalToString.call(this); };但这种做法也有风险因为瑞数会检测Function.prototype.toString是否被篡改。更安全的是只对少数暴露面做拦截不要全局重写。3.2.4 时间戳、随机数与性能检测瑞数会对比Date.now()、performance.now()、Math.random()之间的关系。如果一切太“规律”比如每次performance.now()都是固定递增或者Math.random()被 mock 成线性值很容易被识别出来。补环境的时候建议用真实时间来源控制好精度不要过度伪造。说到底瑞数 6代补环境工程量很大想“通杀”全网所有站点是不现实的。我的建议是能用 playwright 就别死磕补环境补环境只作为降本手段且只针对你固定要访问的少数几个目标网站。3.3 把动态 cookie 接回 requests/session无论用哪种方式拿到动态 cookie最后一步都是接入实际请求库。这里踩坑最多的地方是 cookie 的域、路径和 UA 一致性。下面是一段通用的接入示例import requests def cookie_dict_to_session(cookie_dict: dict, user_agent: str) - requests.Session: session requests.Session() session.headers.update({ User-Agent: user_agent, Accept-Language: zh-CN,zh;q0.9, Referer: https://example.com/, }) for name, value in cookie_dict.items(): session.cookies.set(name, value, domainexample.com, path/) return session有几个细节必须强调UA 必须和获取 cookie 时一致。playwright 浏览器里设置的 UA 是什么requests 里就必须带什么。很多新手换了自己的默认 UA结果 cookie 直接无效。Referer 也要是对应页面。瑞数部分校验会看请求的来源页尤其是首次请求的接口路径和页面路径之间的关系。最好把Referer设置为动态 cookie 对应的页面 URL。cookie 的域名和 path。requests.Session 里设置时如果直接session.cookies.set(name, value)默认 domain 是空某些服务端会忽略掉。建议显式指定域名和path/。验证 cookie 是否过期。瑞数动态 cookie 通常不是永久有效的有的是几分钟有的二十几分钟。建议在模板里设置一个“有效性检测接口”每次业务请求失败时先调检测接口排除 cookie 过期问题。4. 常见问题与排查技巧实录4.1 问题速查表我在做案例五的过程中前前后后遇到过不少问题这里整理成一张速查表方便你快速定位现象可能原因解决办法浏览器打开后一直停在空白页/循环刷新瑞数检测到自动化特征不给正常页面关闭 headless去掉所有 automation 痕迹检查网络请求是否被拦拿到了 cookie但请求业务接口仍然 400UA/Referer 不一致或 cookie 绑定域名不对逐一比对浏览器请求头尤其是 UA、Referer、Accept-Languageplaywright 取 cookie 特别慢页面加载大量静态资源用 route 拦截图片/字体只保留 js/cssNode 补环境执行报错window is undefined环境对象没有完整构建用 Proxy 基座逐步补齐不要直接 defineProperty 全部写死补环境能执行但算出的 cookie 无效环境检测项没有对齐真实浏览器指纹抓取真实浏览器的 canvas/WebGL/AudioContext 指纹做静态回填目标站点 JS 里有无限 debugger瑞数内置反调试在 playwright 里加page.add_init_script覆盖debugger语句cookie 用了几次后失效cookie 有效期短或使用频率异常触发风控做 cookie 池轮换定期重新获取4.2 避坑经验最容易被忽视的检测点最后分享几个常规文档里很少提、但实测非常关键的细节。第一个是mimeTypes和plugins数组。很多补环境脚本只写了navigator.plugins.length 0但瑞数会遍历 plugin 的名称、描述和mimeTypes的具体内容。纯 Node 环境里这些属性是空的所以必须手工维护一份常见的 Chrome 插件列表比如Chrome PDF Plugin、Chrome PDF Viewer、Native Client等。第二个是window.chrome对象。real Chrome 环境里window.chrome及其一些方法比如loadTimes、csi是存在的。补环境时如果不加瑞数直接可以根据“chrome 对象不存在”判定为异常。第三个是document.readyState和事件循环顺序。瑞数 JS 会监听DOMContentLoaded、load等事件有些逻辑在事件回调里触发。playwright 里用wait_untildomcontentloaded返回时事件可能还没完全跑完所以后面留出轮询时间。而补环境时得自己模拟触发这些事件。第四个是不要一次性把所有环境属性写死。瑞数 6代会做一致性校验比如window.innerWidth和document.documentElement.clientWidth要一致navigator.maxTouchPoints和系统平台要匹配。如果只改单一属性反而更容易暴露。第五个是关于“无限 debugger”的绕过位置。不要只靠 playwright 的脚本注入去覆盖因为瑞数有Function.prototype.constructor层面的检测。比较稳的做法是在page.add_init_script里同时覆盖eval和debugger相关方法并且对 target 网站单独做 iframe 处理。这些坑说实话都挺磨人但踩过去之后再遇到瑞数保护的站点就会从容很多。我在实际项目里体会最深的就是“通杀模板”本质上是给自己留了一套标准化排查流程。它不保证你永远不挂但能让你在瑞数更新时用最快速度定位到底是环境变了、混淆变了还是 cookie 策略变了。尤其是 6代补环境有些检测点必须要结合真实浏览器去“采指纹”然后再回到 Node 环境里模拟整个过程非常像教学里的“灰盒测试”。所以我的建议是如果只是业务需要拿数据优先用 playwright 快速解决如果真的是研究型项目再去啃补环境。把这两套思路放在同一个模板框架里你就能在大多数瑞数站点面前少走弯路。