1. 项目概述与核心挑战

最近在分析一个药品信息查询平台时,遇到了一个典型的“瑞数6”动态防护。这个平台的数据接口,每次请求都带有一个名为 sign 的参数,这个参数值每次都在变化,并且看起来毫无规律。如果你直接用浏览器访问,页面正常加载,数据也能出来;但一旦你试图用脚本或者抓包工具去模拟请求,立刻就会返回一堆乱码或者直接403。这就是瑞数动态安全技术(俗称“瑞数6”)的典型特征——它通过动态混淆的JavaScript代码,在客户端生成一个用于验证的令牌,服务端只有拿到正确的令牌才会放行。这个 sign 参数,就是本次逆向分析的核心目标。

对于从事数据采集、接口测试或者安全研究的同行来说,瑞数6是一个绕不开的“硬骨头”。它不像传统的验证码或者简单的Token,其核心逻辑被高度混淆和动态化,直接静态分析JS文件如同大海捞针。本次分析的目标,就是彻底拆解这个药品查询接口的 sign 生成逻辑,并实现一个稳定、可复用的生成方案。整个过程不仅涉及JavaScript逆向,还涉及到浏览器环境模拟、代码动态调试以及逻辑还原,是一次非常综合的实战演练。无论你是想学习瑞数6的对抗思路,还是急需解决某个具体网站的采集难题,相信这篇详细的记录都能给你提供清晰的路径和实用的技巧。

2. 逆向环境搭建与初步探测

2.1 工具选型与配置

工欲善其事,必先利其器。面对瑞数6,选择合适的工具链是成功的第一步。我的核心工具组合是 Chrome DevTools + Node.js + 一个轻量级HTTP拦截代理

  • Chrome DevTools :这是主战场。务必使用无痕模式,并禁用缓存(Network面板勾选“Disable cache”),避免旧JS文件干扰。在Sources面板中,学会使用“Pretty-print”格式化混淆代码,这是阅读天书的第一步。
  • Node.js :用于最终将逆向出来的JavaScript逻辑独立运行。我们需要一个能模拟浏览器部分环境(如 window , document 对象)但不启动完整图形界面的环境。我选择 puppeteer playwright 的核心 CDP 协议能力,或者更轻量的 jsdom 来补环境。
  • HTTP拦截代理 :推荐使用 mitmproxy Fiddler 。它们不仅能抓包,更重要的是可以断点修改请求/响应。在初步探测阶段,用于确认哪些请求被瑞数保护(观察是否有 Set-Cookie 包含类似 __jsluid_s , __jsl_clearance_s 的字段),以及 sign 参数出现在哪个请求里。

注意 :不要一上来就用自动化脚本狂发请求,这很容易触发IP频率限制甚至封禁。初期所有操作尽量在浏览器内手动进行,摸清规律后再上脚本。

2.2 请求流程分析与关键点定位

首先,在浏览器中正常访问一次药品查询页面,例如搜索“阿司匹林”。打开DevTools的Network面板,仔细查看请求瀑布流。

  1. 首次请求 :通常第一个HTML文档请求会返回一个状态码为 200 但内容是一段高度混淆的JavaScript代码的响应。这段代码就是瑞数的“挑战”代码,它包含了生成第一个验证令牌( jsluid jslclearance )的逻辑。
  2. Cookie设置 :执行完上述JS后,浏览器会自动重发请求,并携带新生成的Cookie( __jsluid_s __jsl_clearance_s )。服务端验证通过后,才会返回真正的页面HTML。
  3. 数据接口请求 :页面加载后,当你进行查询操作时,会发起XHR或Fetch请求到数据接口。 此时, sign 参数出现了 。它通常作为查询参数(如 ?keyword=阿司匹林&sign=xxxxxx )或请求体的一部分。关键是要找到生成这个请求的JavaScript源。
  4. 定位入口 :在Network面板中找到携带 sign 的接口请求,右键选择“Copy -> Copy as cURL”或“Copy -> Copy as Node.js fetch”。然后,在Sources面板的“Search”功能中,搜索 sign 这个关键词,或者搜索该接口的URL路径。通常能定位到一处或几处相关的JavaScript文件。这些文件往往也被混淆过,文件名可能是一串无意义的哈希值。

通过以上步骤,我们确定了几个关键点:瑞数6的防护是分阶段的(先过Cookie关,再过业务接口关); sign 的生成逻辑藏在某个被请求的JS文件中;这个JS文件是动态的,每次可能不同,但核心算法可能被抽取在某个固定模块中。

3. 核心JS逻辑逆向与动态调试

3.1 代码格式化与关键函数追踪

找到疑似生成 sign 的JS文件后,第一件事就是点击左下角的 {} 按钮进行代码美化。面对混淆后的代码,不要试图从头到尾理解。我们的策略是“顺藤摸瓜”。

  1. 搜索关键字符串 :在美化后的代码中,搜索 sign encrypt encode param 等关键词。很可能你会发现类似 sign: s params.sign = c 这样的赋值语句。
  2. 设置断点 :在找到的赋值语句所在行设置行断点。然后,在网页上再次执行一次查询操作。浏览器会暂停在断点处。
  3. 调用栈分析 :这是最关键的一步!当断点触发时,立即查看右侧的“Call Stack”(调用栈)。调用栈显示了当前函数是被谁一层层调用的。从栈顶往下看,找到第一个不属于大型库(如jQuery、Vue)的、文件名看起来是项目本身的函数,点击跳转进去。
  4. 回溯与定位 :通过反复使用调用栈回溯,我们最终的目标是找到那个最原始的、接收原始参数(如查询关键词、时间戳等)并计算出 sign 值的函数。我们把这个函数称为 generateSign calcSign

3.2 环境依赖分析与补环境策略

在动态调试时,你可能会发现 generateSign 函数内部依赖了一些浏览器特有的对象或API,例如 window.atob , document.createElement , navigator.userAgent ,甚至是 WebAssembly 模块。这就是瑞数增加逆向难度的常用手段——将核心逻辑与浏览器环境深度绑定。

这时,需要记录下所有依赖的外部对象和函数。例如:

// 伪代码示例
function generateSign(params) {
    var a = window.btoa(params.keyword);
    var b = new Uint8Array(someArray);
    var c = window.crypto.subtle.digest('SHA-256', b); // 异步API
    // ... 更多逻辑
}

对于 btoa Uint8Array 这类标准API,Node.js 环境或 jsdom 通常原生支持或容易模拟。但对于 window.crypto.subtle 这类较新或浏览器特有的API,就需要手动“补环境”。

补环境的核心思路 :在Node.js中,创建一个模拟的 window document 对象,将缺失的属性或函数按需实现。对于复杂的加密操作,有时瑞数会将自己的算法打包在一个立即执行的函数表达式里,我们只需要把这个函数整体提取出来,并在一个模拟了必要环境(如 window document 对象存在)的上下文中执行它。

实操心得 :不必追求100%完美模拟整个浏览器环境。我们的目标是让 generateSign 函数能运行并输出正确结果。因此,采用“缺啥补啥”的策略最高效。用 typeof window.xxx === 'undefined' 来判断是否需要补,补的时候尽量用简单的实现,只要不报错且不影响核心计算逻辑即可。例如, document.createElement 可以补一个返回空对象 {} 的函数。

3.3 算法还原与参数提取

在能够单步调试 generateSign 函数后,下一步就是厘清它的算法。

  1. 输入参数 :观察函数接收哪些参数。通常是业务参数(如 { keyword: “阿司匹林”, page: 1 } )和一个可能固定的 salt (盐值)或 secret (密钥)。这个 secret 可能硬编码在JS中,也可能从之前某个接口响应中获取。
  2. 处理流程
    • 参数排序 :常见做法是将所有参数按键名ASCII码升序排序,拼接成 key1=value1&key2=value2 的字符串。
    • 字符串拼接 :将排序后的参数字符串,加上一个 secret (或时间戳、随机数等),组合成一个新的字符串。
    • 哈希/加密 :对这个新字符串进行某种哈希运算(如MD5、SHA-1、SHA-256)或对称加密(如AES)。瑞数6比较喜欢用 SHA-256 或自定义的位运算。通过调试,观察中间变量值,可以确定具体算法。例如,调试时看到 crypto.subtle.digest(‘SHA-256’, data) 这样的调用,就能确定是SHA-256。
    • 编码输出 :将哈希/加密后的二进制结果,进行Base64或Hex编码,最终生成 sign 字符串。
  3. 验证算法 :在浏览器调试器中,手动修改输入参数,执行 generateSign 函数,将输出的 sign 与真实网络请求中的 sign 进行对比。一致,则算法还原成功。

4. 独立签名生成器的实现

4.1 Node.js 环境下的代码移植

算法搞清楚后,就要把它从浏览器环境“移植”到Node.js中,形成一个独立的签名生成模块。

  1. 提取核心函数 :将调试定位到的 generateSign 函数及其所有直接依赖的内部函数,从混淆的JS文件中完整地提取出来。可以使用DevTools中的“Copy function definition”功能,但要注意它可能不会复制闭包内的依赖函数,需要手动一并提取。
  2. 创建补环境脚本 :新建一个Node.js文件(如 sign_generator.js )。开头部分,创建模拟的全局对象。
    // 补环境
    if (typeof global !== 'undefined') {
        global.window = global;
        global.document = {
            createElement: function() { return {}; },
            // ... 其他可能用到的属性
        };
        // 补全特定的加密API,例如使用Node.js原生crypto模块模拟
        const crypto = require('crypto');
        global.window.crypto = {
            subtle: {
                digest: function(algorithm, data) {
                    // 将算法名映射到Node.js crypto
                    const hash = crypto.createHash(algorithm.replace('-', '').toLowerCase());
                    hash.update(data);
                    return Promise.resolve(hash.digest());
                }
            }
        };
        global.btoa = (str) => Buffer.from(str).toString('base64');
        global.atob = (b64Encoded) => Buffer.from(b64Encoded, 'base64').toString();
    }
    
  3. 注入算法 :将提取出的核心函数代码(通常是几个相互嵌套或调用的函数)粘贴到补环境代码的后面。
  4. 封装调用接口 :最后,导出一个简洁的调用函数。
    // 假设核心函数叫 `window.generateSign`
    module.exports.generateSign = function(queryParams, secretKey) {
        // 这里可能需要对参数做一些预处理,以匹配浏览器中的调用方式
        return window.generateSign(queryParams, secretKey);
    };
    

4.2 稳定性测试与异常处理

生成器写好后,不能立刻投入生产环境,需要进行充分的测试。

  1. 多参数测试 :使用不同的查询关键词、页码、时间戳等进行测试,生成 sign ,并与浏览器在同一时刻发起的请求中的 sign 进行比对,确保完全一致。
  2. 长时间运行测试 :连续运行生成器几个小时,模拟批量查询场景,观察是否有内存泄漏或偶然的签名错误。瑞数的JS可能依赖某些具有“状态”的全局变量,需要确保在Node.js中每次调用时环境是干净且一致的。
  3. 异常处理
    • 参数缺失 :检查必要参数是否传入。
    • 环境缺失 :捕获因环境模拟不全导致的 ReferenceError ,并给出友好提示。
    • 网络依赖 :如果 secretKey 需要从网络接口定时获取,需要增加获取失败的重试机制和缓存逻辑。
  4. 性能考量 :如果 generateSign 函数涉及复杂的循环或WebAssembly调用,可能会成为性能瓶颈。在Node.js中,可以考虑将计算密集部分放入工作线程,或者对固定参数的签名结果进行短期缓存(注意 sign 通常具有时效性)。

5. 集成到数据采集流程与风控应对

5.1 完整请求链路的构建

有了可靠的 sign 生成器,就可以构建完整的自动化请求链路了。流程如下:

  1. 初始化会话 :使用 puppeteer playwright 启动一个真实浏览器实例,访问目标网站首页,让浏览器自动完成瑞数第一阶段的Cookie验证(获取 __jsluid_s __jsl_clearance_s )。这一步虽然重,但最稳妥,能拿到合法的会话基础。
  2. 提取关键信息 :从成功加载的页面中,通过 page.evaluate() 执行JS,提取出生成 sign 所需的 secretKey (如果它是动态注入到页面中的)。同时,将必要的Cookie提取出来。
  3. 签名与请求 :在Node.js主逻辑中,调用本地化的 sign 生成器,为每一个查询请求生成实时 sign
  4. 发送请求 :使用 axios node-fetch 等HTTP库,携带之前获取的Cookie和刚生成的 sign ,向数据接口发送请求。 此时应使用与浏览器一致的请求头 ,特别是 User-Agent Referer Accept-Language 等。
  5. 数据处理 :解析接口返回的JSON数据。

5.2 常见风控特征与规避策略

即使 sign 正确,请求仍可能被拦截。需要关注以下风控点:

风控特征 可能表现 规避策略
IP频率 请求过快导致IP被临时封禁,返回403或验证码。 在请求间添加随机延迟(如 2~5秒)。使用高质量代理IP池,并轮换使用。
请求头指纹 缺少或存在不合理的请求头。 完整复制浏览器请求的所有Headers,特别是 Accept , Accept-Encoding , Connection
TLS指纹 服务器检测到请求来自非浏览器客户端(如 curl , requests 库)。 使用能修改TLS指纹的库,如 tls-client (Python)或 curl 的特定版本。Node.js的 axios 默认指纹较易识别,可尝试使用 puppeteer page.setRequestInterception 来代理请求,直接利用浏览器的网络栈。
行为模式 查询模式过于规律(如每秒一次,关键词顺序固定)。 模拟人类操作:随机化查询间隔,在查询中穿插翻页、点击等无关操作(如果使用浏览器自动化)。
Cookie失效 __jsl_clearance_s 有过期时间(通常较短)。 监控请求响应,一旦出现签名错误或重定向到挑战页面,立即触发重新获取Cookie的流程。

重要提示 :最稳健但效率较低的方式,是全程使用浏览器自动化工具(如 puppeteer )来执行操作和获取数据。将 sign 生成器本地化,是为了在必要时绕过浏览器执行JS的性能开销,实现更高效率的纯HTTP请求。在实际项目中,往往采用“ 浏览器维护会话 + 本地签名发起请求 ”的混合模式,在稳定性和效率之间取得平衡。

6. 疑难问题排查与深度优化

6.1 签名突然失效的排查步骤

这是最常遇到的问题。如果之前正常的签名突然开始被服务器拒绝,请按以下顺序排查:

  1. 检查Cookie :首先确认用于请求的 __jsluid_s __jsl_clearance_s 是否仍然有效。尝试在浏览器中打开新页面,看是否需要重新验证。Cookie失效是首要怀疑对象。
  2. 验证算法输入 :确认生成 sign 的输入参数是否发生了变化。特别是 secretKey ,它可能每小时或每天从服务器端更新一次。你需要重新从页面中提取这个值。
  3. 对比浏览器请求 :在签名失效的时间点,手动在浏览器中进行一次相同的查询操作。抓取这次成功请求的 sign 值,以及请求的所有参数(包括URL参数和请求体)。与你本地生成器使用的参数进行逐字段比对。
  4. 检查时间戳 :很多 sign 算法会加入当前时间戳(到秒或毫秒)来防止重放攻击。确保你的服务器时间与目标网站服务器时间基本同步(误差在几分钟内)。如果算法使用时间戳,你生成签名的时间必须和发起请求的时间非常接近。
  5. 查看JS文件是否更新 :瑞数可能会不定期更新前端JS的混淆方式或算法细节。重新访问网站,查看生成 sign 的核心JS文件内容是否发生了变更(对比文件哈希或关键函数结构)。

6.2 性能优化与代码维护

当你的签名生成器需要处理海量请求时,性能和维护性就变得很重要。

  1. 缓存策略
    • 静态资源缓存 :从页面中提取的 secretKey 和核心JS代码,如果在一定时间内不变,可以缓存在内存或Redis中,避免每次请求都去访问页面。
    • 签名结果缓存 :对于相同的输入参数,其 sign 在有效期内是固定的。可以建立一个短期缓存(例如5秒),键为参数排序后的字符串,值为 sign 。这能极大减少计算量。 但务必注意缓存的过期时间必须短于 sign 的实际有效期
  2. 代码模块化与监控 :将补环境、算法核心、请求逻辑拆分成独立模块。为签名生成函数添加详细的日志,记录输入、输出、耗时。当签名失败时,能快速定位是哪个环节出了问题。
  3. 降级方案 :始终准备一个降级方案。当本地签名生成器因网站更新而大面积失效时,应能自动切换回使用完整的浏览器自动化( puppeteer )来执行查询,虽然慢但能保证业务不中断,为你修复签名生成器争取时间。

整个逆向瑞数6并实现 sign 生成的过程,是一场与防护方案设计者的智力博弈。它没有一成不变的解决方案,需要你具备耐心细致的调试能力、对Web技术的深刻理解以及灵活的应变思维。每一次成功的逆向,不仅解决了一个具体的数据获取问题,更是对自身技术栈的一次强力升级。记住,尊重网站的 robots.txt 协议,控制请求频率,将数据用于合法合规的用途,是每一位技术从业者应尽的义务。

更多推荐