Python爬虫实战:逆向瑞数6代动态Cookie生成机制

📅 2026/7/7 19:55:54
Python爬虫实战:逆向瑞数6代动态Cookie生成机制
1. 项目概述与核心挑战最近在做一个学术数据整合的小工具需要从几个主流的中文期刊服务平台抓取文献元数据。本以为这类学术网站的反爬措施会比较温和毕竟内容相对公开但实际一上手就碰了个硬钉子。目标网站使用了瑞数6代RS6的动态安全技术所有关键数据请求都依赖一组由前端JavaScript动态生成的、长度超长的Cookie。这直接导致常规的requests库配合Session对象完全失效请求返回的不是数据而是一堆混淆的、用于反爬验证的脚本代码。这让我意识到不搞定这个JS逆向后续的数据采集工作根本无从谈起。瑞数6代是当前Web应用安全领域里一块相当难啃的骨头它通过动态混淆的JavaScript代码在浏览器端实时生成加密令牌也就是那串长长的Cookie服务端通过验证这个令牌来区分真实用户和自动化脚本。对于爬虫开发者来说这意味着你不能简单地复制浏览器里的Cookie来用因为它是“活”的有过期时间且生成逻辑被深度混淆和动态变化。本次实战的目标就是深入这个“黑盒”理解其Cookie的生成机制并最终在Python环境中模拟出这一过程实现稳定、可复用的数据请求。整个过程不仅是一次技术攻坚更是一次对现代前端反爬技术原理的深度探索。我会从环境判断、动态调试、代码提取、环境补全到最终调试验证一步步拆解并分享其中遇到的坑和解决思路。无论你是想学习JS逆向的入门思路还是正在被类似的反爬问题困扰希望这篇详尽的记录都能给你带来启发。2. 逆向目标分析与技术研判2.1 目标网站特征识别首先我们需要明确攻击目标。打开目标期刊网站的搜索页或详情页按F12打开开发者工具切换到“网络”(Network)标签页。清空记录后刷新页面你会观察到一系列网络请求。我们的关注点在于那些携带了超长、看似乱码Cookie的请求通常是向数据API接口发起的XHR或Fetch请求。关键判断依据有以下几点Cookie特征在请求头中寻找名为RM4hZBv0dDon443M、FSSBBIl1UgzbN7N80T等格式的Cookie其值通常是一长串超过200位甚至500位由字母和数字组成的字符串。这是瑞数动态令牌的典型命名。“本地生成”标记在开发者工具的“应用程序”(Application)标签页中查看“存储”-“Cookie”。找到上述特征Cookie观察其“路径”、“域”等信息。一个更直接的证据是在“网络”请求的详情中查看该Cookie的“来源”列。如果显示为“脚本”而非“网络”则基本可以断定该Cookie是由前端JavaScript代码本地计算生成的而非服务器直接下发。初始化请求在页面加载初期通常会有几个关键的.js或.chunk.js文件被加载其内容经过高度混淆无法直接阅读。这往往是瑞数核心逻辑的载体。在我的目标网站上首次数据请求的Cookie中就包含了RM4hZBv0dDon443M这种格式的字段且在Cookie列表中明确标记为“脚本”生成。结合其超过400位的长度基本可以锁定为瑞数6代防护。2.2 瑞数6代技术原理浅析为什么瑞数这么难对付它的核心思想是“动态”和“混淆”。动态代码每次页面加载核心的JavaScript代码结构、变量名、函数名都可能发生变化静态分析代码几乎不可能。环境依赖生成令牌的代码极度依赖浏览器环境它会检测大量的浏览器特有对象、属性和方法例如window、document、navigator、screen、location甚至包括一些Web API的行为特征。逻辑混淆代码被压缩、变量被替换成无意义的短字符、控制流被扁平化或打乱让人一眼看去如同天书。自执行函数核心逻辑通常包裹在一个立即执行的函数表达式(IIFE)中该函数在页面加载时运行完成环境检测和令牌计算。因此我们的逆向策略不是去“读懂”每一行混淆后的代码那效率太低而是“模拟”出一个足够真实的浏览器环境让这段混淆代码在我们控制的环境如Node.js或Python的JS执行引擎中能够顺利运行并输出正确的Cookie值。这个过程在圈内通常被称为“补环境”。注意在进行任何逆向操作前务必仔细阅读网站的robots.txt文件和服务条款。明确网站是否允许爬虫以及允许的爬取频率。我们的所有操作应仅限于技术学习与研究避免对目标网站服务器造成压力。在实际项目中必须严格遵守robots.txt的约定并添加合理的请求间隔如每次请求间隔2-5秒。3. 逆向调试与核心代码提取3.1 调试环境准备与断点设置工欲善其事必先利其器。浏览器开发者工具是我们的主战场。打开无痕窗口使用Chrome或Edge浏览器的无痕模式避免浏览器插件和缓存干扰。启用调试工具按F12确保“源代码/来源”(Sources)面板可用。设置事件断点这是关键一步。在“Sources”面板右侧找到“事件监听器断点”(Event Listener Breakpoints)区域。展开“脚本”(Script)子项勾选“脚本首次执行”(Script First Statement)或“脚本执行”(Script Execution)。这个断点能帮助我们在页面JS刚开始执行时就暂停方便我们追踪初始化流程。清除状态在“应用程序”(Application) - “存储” - “Cookie”中右键删除目标网站的所有Cookie。然后刷新页面。刷新后浏览器会在执行第一条JS语句前自动暂停。此时我们已成功“冻结”了页面的初始状态。3.2 追踪Cookie生成流程在调试器暂停的状态下我们的目标是找到生成目标Cookie如RM4hZBv0dDon443M的那段代码。单步执行与观察使用“单步跳过”(F10)或“单步执行”(F11)逐步运行代码。同时保持“网络”(Network)面板开启并勾选“保留日志”(Preserve log)。当观察到第一个携带目标Cookie的请求出现时停止单步。定位关键函数这个请求的Cookie是谁设置的我们可以在“调用堆栈”(Call Stack)面板中查看当前暂停位置是由哪个函数发起的请求。通常设置Cookie的代码会在请求发起前的某个函数中。你需要逆向查找调用栈找到一个包含document.cookie赋值操作或对某个特定对象属性进行赋值的代码段。识别核心代码块找到疑似生成Cookie的代码区域后你会发现它通常位于一个巨大的、被压缩成几行的混淆代码文件中。这段代码可能包含大量的数组操作、位运算和字符串拼接。记住这个文件的名字和大概位置。3.3 提取与保存混淆代码我们无法直接分析混淆代码但可以把它“偷”出来。格式化与复制在“Sources”面板中找到那个核心的混淆JS文件。点击左下角的{}美化代码按钮让代码变得可读尽管逻辑依然混乱。选中从文件开头到结尾的所有代码复制。保存核心逻辑将复制的内容粘贴到一个本地文件中例如core_obfuscated.js。重要提示瑞数有反格式化检测。直接复制美化后的代码可能包含格式化标记导致在本地执行时报错。更稳妥的做法是在复制前不要点击美化直接复制那一行长长的、压缩的代码。或者使用浏览器控制台(Console)执行copy(‘整个文件内容’)命令来获取原始压缩代码。寻找入口点仅仅有核心逻辑还不够我们需要找到触发这个逻辑的“入口函数”。通常在页面HTML中或另一个初始化JS文件中会有一个自执行函数来调用核心逻辑。通过调试时的调用堆栈你可以找到这个入口函数所在的文件。用同样的方法将其代码复制保存例如entry.js。至此我们拿到了“原料”高度混淆的核心算法代码和启动它的入口代码。4. 本地环境模拟与补环境实战这是整个逆向过程中最考验耐心和细心的环节。我们需要在Node.js环境中构造一个足以“欺骗”混淆代码的浏览器环境。4.1 基础环境搭建首先创建一个Node.js项目并安装必要的模块。我们主要会用到vm2来创建一个相对安全的沙箱环境执行JS或者直接用node的vm模块。mkdir rs6_reverse cd rs6_reverse npm init -y创建一个主文件simulate.js我们将在这里进行补环境操作。4.2 补环境的核心思路与步骤补环境不是一蹴而就的而是一个“运行-报错-补充-再运行”的循环过程。创建基础Window对象在浏览器中window是全局对象。在Node中我们需要手动构建。// simulate.js const vm require(vm); const fs require(fs); // 1. 创建一个空的沙盒上下文 const sandbox { window: {}, document: {}, navigator: {}, location: {}, // ... 其他可能需要的全局对象 console: console, // 方便调试输出 }; // 让 window 指向自己并作为全局对象的代理 sandbox.window sandbox; sandbox.self sandbox.window; sandbox.top sandbox.window; sandbox.parent sandbox.window; // 2. 加载我们之前保存的混淆核心代码 const coreCode fs.readFileSync(./core_obfuscated.js, utf-8); const entryCode fs.readFileSync(./entry.js, utf-8); // 3. 尝试执行入口代码 try { const script new vm.Script(entryCode \n coreCode); // 合并执行 script.runInNewContext(sandbox); console.log(执行成功); } catch (error) { console.error(执行报错:, error.message); console.error(第一个未定义的属性或方法很可能就是需要补的:, error.stack); }第一次运行百分之百会报错提示某个对象或属性undefined。迭代补充缺失对象根据错误信息补充例如报错Cannot read property document of undefined说明代码试图访问window.document但我们只给了window一个空对象。我们需要给sandbox.window添加document属性同样初始化为一个空对象{}。模拟浏览器API很多错误是关于具体方法的比如document.createElement。我们需要在sandbox.document上实现这个方法。sandbox.document { createElement: function(tagName) { console.log([模拟] document.createElement被调用参数: ${tagName}); // 返回一个模拟的DOM元素对象 if (tagName.toLowerCase() div) { return { tagName: DIV, style: {}, getElementsByTagName: function(name) { return []; }, // 先简单返回空数组 // ... 其他div可能有的属性和方法 }; } // 简单处理其他标签 return { tagName: tagName.toUpperCase(), style: {} }; }, cookie: , // 用于存储和读取cookie getElementsByTagName: function(name) { return []; }, // 逐步补充其他常用属性和方法如 document.body, document.documentElement等 };使用Proxy进行动态拦截这是更高级和高效的方法。我们可以用一个Proxy对象来包装window或document当代码访问一个不存在的属性时我们动态地创建并返回一个模拟值或函数同时打印日志这能极大帮助我们了解代码运行时的依赖。const createHandler (name) ({ get: function(target, prop, receiver) { if (prop in target) { return target[prop]; } // 如果访问不存在的属性动态创建一个空函数或对象并记录 console.log([动态补全] ${name}.${prop} 被访问类型未定义动态创建空函数。); const dummyFunc () { console.log([模拟调用] ${name}.${prop}()); // 根据常见API返回一些默认值 if (prop toString) return () [object Object]; return null; }; dummyFunc.toString () function ${prop}() { [native code] }; target[prop] dummyFunc; return dummyFunc; }, set: function(target, prop, value) { target[prop] value; return true; } }); sandbox.window new Proxy({}, createHandler(window)); sandbox.document new Proxy({cookie: }, createHandler(document));处理特定陷阱无限Debugger有些瑞数实现会包含debugger;语句或通过setInterval不断触发调试器。在Node环境中debugger;语句无效。对于定时器我们需要模拟setInterval和setTimeout让它们立即执行或返回一个空ID。sandbox.window.setInterval function(fn, delay) { console.log([模拟] setInterval被调用delay: ${delay}); // 可以立即执行一次或者什么都不做 // fn(); return 12345; // 返回一个模拟的定时器ID }; sandbox.window.setTimeout sandbox.window.setInterval; // 类似处理 sandbox.window.clearInterval sandbox.window.clearTimeout function() {};浏览器特性检测代码可能会检测navigator.userAgent、screen.width/height、location.href等。我们需要提供合理的模拟值。sandbox.navigator { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, platform: Win32, // ... 其他属性 }; sandbox.location { href: https://目标网站域名.com/, protocol: https:, hostname: 目标网站域名.com, // ... }; sandbox.screen { width: 1920, height: 1080, colorDepth: 24, // ... };4.3 关键突破口Cookie长度验证与深度补全按照上述方法迭代补充你可能发现代码能运行到最后并且sandbox.document.cookie里也生成了一个值。但是当你兴冲冲地拿着这个Cookie去请求接口时很可能被服务器拒绝。一个非常关键的验证点来了Cookie长度。对比长度用浏览器正常访问一次在开发者工具中复制出完整的、有效的RM4hZBv0dDon443MCookie值记录其长度字符数。然后将你本地模拟生成的Cookie值长度与之对比。长度不足的根源如果模拟生成的Cookie长度明显短比如浏览器是400位你生成的只有200位说明还有关键的环境检测没有通过导致生成逻辑提前终止或分支走向错误。混淆代码中可能存在一些条件判断依赖于某个我们尚未模拟的、非常隐蔽的浏览器特性或API返回值。深度排查开启详细日志在Proxy的get陷阱中不仅打印属性名还可以打印访问的路径深度这有助于发现那些深层嵌套的属性访问。对比浏览器行为在浏览器调试器中在生成Cookie的关键函数附近打上断点。观察执行过程中哪些对象、属性被频繁访问或计算。特别是那些返回undefined、null或特定类型值如[object HTMLDivElement]的地方。确保你的模拟环境返回了类型一致的值。例如document.createElement(‘div’)在浏览器中返回的是一个真实的HTMLDivElement对象其toString()的结果是[object HTMLDivElement]。如果你的模拟对象返回的是普通对象{}toString()的结果是[object Object]就可能导致后续的类型检查失败。补全DOM原型链一些代码可能会调用div.appendChild、div.getAttribute等方法或者检查div.nodeType、div.firstChild等属性。你需要构建一个更完整的模拟DOM元素其原型链上挂载了必要的方法。function createDOMElement(tagName) { const elem { nodeType: 1, tagName: tagName.toUpperCase(), style: {}, attributes: {}, childNodes: [], parentNode: null, getAttribute: function(name) { return this.attributes[name]; }, setAttribute: function(name, value) { this.attributes[name] value; }, appendChild: function(child) { this.childNodes.push(child); child.parentNode this; return child; }, removeChild: function(child) { /* ... */ }, // ... 其他必要方法 }; // 重写toString方法使其更像浏览器原生对象 elem.toString () [object HTML${elem.tagName}Element]; return elem; } // 更新 document.createElement sandbox.document.createElement function(tagName) { return createDOMElement(tagName); };通过反复的“运行-对比-补充”循环当你的本地模拟代码生成的Cookie长度与浏览器生成的完全一致并且结构看起来也相似时就成功了一大半。5. 集成到Python爬虫与请求验证5.1 构建Cookie生成函数当你的Node.js环境可以稳定生成正确长度的Cookie后下一步是将这个生成逻辑封装成一个独立的、可调用的函数。通常混淆代码的入口函数会接收一些参数可能是当前时间戳、页面URL等并返回最终的Cookie字符串。你需要分析入口函数的调用方式。在浏览器调试器中找到调用入口函数的地方看看它传入了什么参数。然后在你的simulate.js中将这个入口函数暴露给沙盒外部。// 在sandbox中暴露一个全局函数或变量用于触发计算 sandbox.getDynamicCookie function() { // 这里调用混淆代码中的核心函数假设它叫 generateRsCookie // 你需要根据实际分析出的函数名和参数来调用 const args [Date.now(), window.location.href]; // 示例参数 return window.generateRsCookie.apply(null, args); // 假设生成函数挂载在window上 }; // ... 执行代码后 const dynamicCookie sandbox.getDynamicCookie(); console.log(生成的Cookie:, dynamicCookie);5.2 使用PyExecJS或Node子进程调用Python不能直接执行复杂的、依赖浏览器环境的JS代码。我们有几种桥接方案方案一PyExecJS适合逻辑相对独立、环境依赖已补全PyExecJS是一个Python库可以调用系统上的JavaScript运行时如Node.js。import execjs # 读取我们补全环境后的完整JS代码 with open(simulate_with_env.js, r, encodingutf-8) as f: js_code f.read() # 创建上下文并执行 ctx execjs.compile(js_code) # 调用我们暴露的函数 cookie_value ctx.call(getDynamicCookie) print(f生成的Cookie: {cookie_value})方案二Node.js子进程更稳定、性能更好推荐将Cookie生成逻辑写成一个独立的Node.js脚本cookie_generator.js它接收参数输出Cookie。Python通过subprocess模块调用它。// cookie_generator.js const vm require(vm); // ... 所有补环境的代码和核心逻辑 ... function generateCookie(url) { // 根据url等参数计算cookie // ... return cookieString; } // 从命令行参数获取输入输出到stdout const url process.argv[2]; const cookie generateCookie(url); console.log(cookie);# python_crawler.py import subprocess import requests import json def get_rs_cookie(target_url): 调用Node.js脚本生成瑞数Cookie try: # 调用Node脚本并传递URL作为参数 result subprocess.run( [node, cookie_generator.js, target_url], capture_outputTrue, textTrue, checkTrue, timeout10 # 设置超时防止卡死 ) cookie_str result.stdout.strip() # 假设脚本返回的是类似 RM4hZBv0dDon443Mxxxxxx; FSSBBIl1UgzbN7N80Tyyyyyy return cookie_str except subprocess.CalledProcessError as e: print(fNode脚本执行错误: {e.stderr}) return None except subprocess.TimeoutExpired: print(生成Cookie超时) return None def main(): target_url https://目标期刊网站/api/search # 1. 获取动态Cookie cookie_header get_rs_cookie(target_url) if not cookie_header: print(获取Cookie失败) return # 2. 构造请求头 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: application/json, text/plain, */*, Referer: https://目标期刊网站/, Cookie: cookie_header # 使用动态生成的Cookie } # 3. 发起数据请求 params {keyword: 人工智能, page: 1} try: resp requests.get(target_url, headersheaders, paramsparams, timeout15) resp.raise_for_status() # 检查HTTP错误 # 4. 处理响应 if resp.status_code 200: data resp.json() print(请求成功获取数据条数:, len(data.get(list, []))) # ... 后续数据处理逻辑 else: print(f请求失败状态码: {resp.status_code}) except requests.exceptions.RequestException as e: print(f网络请求异常: {e}) if __name__ __main__: main()5.3 请求会话管理与生命周期生成的瑞数Cookie通常有较短的有效期可能几分钟到几十分钟并且可能与特定的会话Session绑定。会话保持使用requests.Session()对象来管理连接和Cookie。将动态生成的Cookie设置到Session中。session requests.Session() # 解析cookie_header字符串将其设置到session.cookies中 from http.cookies import SimpleCookie cookie SimpleCookie() cookie.load(cookie_header) for key, morsel in cookie.items(): session.cookies.set(key, morsel.value)Cookie刷新策略不要每次请求都重新生成Cookie这样效率低且可能触发风控。一个常见的策略是在创建Session时生成一次Cookie。监控请求的响应。如果连续几次请求返回了非200状态码如403、412或返回了包含反爬挑战的HTML/JS而不是JSON数据则判断Cookie可能已失效。当判断Cookie失效时重新调用get_rs_cookie函数生成新的Cookie并更新到Session.cookies中然后重试失败的请求。请求间隔即使在Cookie有效期内也务必在请求之间添加随机延迟例如time.sleep(random.uniform(2, 5))模拟人类操作避免对服务器造成瞬时压力。6. 常见问题排查与实战心得6.1 问题排查清单在逆向和集成过程中你肯定会遇到各种问题。下面是一个快速排查清单问题现象可能原因排查思路与解决方案Node环境执行报错XXX is not defined浏览器环境对象如window,document,navigator或其属性未模拟。1. 检查错误信息中提到的变量名。2. 在浏览器调试器中查看该变量在正常运行时的类型和值。3. 在沙盒环境中补充对应的对象或属性注意类型一致性是函数就返回函数是对象就返回对象。生成的Cookie长度与浏览器不一致环境模拟不完整导致代码执行路径与浏览器不同可能触发了错误分支或提前退出。1.核心方法在浏览器和Node中在生成Cookie的同一行代码后打日志输出中间变量或关键判断条件对比两边的执行路径差异。2. 检查是否漏补了某些深层属性如document.documentElement.clientWidth、navigator.plugins等。3. 使用Proxy全面拦截查看有哪些属性被访问了但你的环境返回undefined。携带Cookie请求返回412/403错误1. Cookie本身错误或已过期。2. 请求头不完整缺少必要的Referer、User-Agent或自定义头部。3. Cookie与其他请求参数如时间戳、签名不匹配。1.对比请求用浏览器正常访问一次在开发者工具中精确复制所有请求头包括Cookie、User-Agent、Accept、Referer等与你Python脚本的请求头逐字段对比。2.检查Cookie有效期在浏览器中查看该Cookie的Expires/Max-Age属性。3.参数联动有些网站生成Cookie时可能用到了页面URL或某个全局变量请求时这个上下文必须一致。确保你生成Cookie时模拟的location.href与最终请求的URL相关。请求返回一段JS代码或空白页Cookie完全无效请求被瑞数拦截返回了反爬的挑战代码。1. 重新生成Cookie。2. 确认你的补环境代码是否因为网站更新而失效瑞数会不定期更新混淆算法。3. 尝试在请求中添加更完整的浏览器指纹头如Accept-Language、Sec-CH-UA等。Node脚本执行超时或卡死1. 模拟代码中存在死循环如未被正确处理的setInterval。2. 环境检测陷入无限递归。1. 确保正确模拟了setInterval和setTimeout避免它们真的创建循环。2. 在Node脚本开头设置超时setTimeout(() process.exit(1), 10000)。3. 在Proxy的get陷阱中记录深度防止对某个属性的无限递归访问。6.2 实战心得与进阶技巧保持环境一致性浏览器环境是庞大而复杂的。我们的目标是“够用就好”但“够用”的边界需要仔细摸索。重点模拟那些被检测到的环境特征而不是盲目模拟所有Web API。使用Proxy进行动态补全是最高效的策略它能自动暴露代码的所有依赖。重视类型和值对于关键的函数调用不仅要模拟函数存在还要让它的返回值类型和值尽可能接近浏览器。例如typeof document.createElement在浏览器中是”function”你的模拟也必须是函数。Object.prototype.toString.call(document.createElement(‘div’))在浏览器中是”[object HTMLDivElement]”你的模拟对象也应该通过重写toString方法来返回相同的字符串。版本应对瑞数6代本身也在迭代。今天逆向成功的代码几个月后可能因为网站更新而失效。这意味着你需要维护一套补环境的基础代码当失效时快速对比新旧两版网页加载的JS文件差异找出新增的环境检测点进行补充。道德与法律边界这是老生常谈但至关重要。你的爬虫应该尊重robots.txt。设置宽裕的请求间隔time.sleep。只爬取公开、非敏感的数据。明确将技术用于学习和研究目的而非商业滥用或攻击。考虑使用网站提供的官方API如果有的话这是最稳定、最合规的方式。逆向瑞数6代是一个繁琐但极具成就感的过程它像是一场与安全工程师的智力博弈。每一次成功绕过都是对浏览器原理和前端安全技术更深一层的理解。这个过程没有银弹需要的是耐心、细致的观察力和不断的实验。希望这篇长文能为你照亮这条路最初的一段。当你的爬虫终于能稳定抓取到数据时那种感觉妙不可言。