爬虫实战:逆向破解JS动态Cookie反爬机制
1. 项目概述当爬虫遇上动态Cookie做爬虫的朋友尤其是刚入门的最头疼的莫过于遇到那些“会动”的Cookie。你兴冲冲地写好了请求把从浏览器开发者工具里复制出来的Cookie字符串贴进去结果发现第一次请求还好好的第二次、第三次就突然失效了返回的不是403就是一堆乱码。这背后大概率就是网站启用了基于JavaScript的动态Cookie反爬机制。这和我们平时理解的静态Cookie完全不同。静态Cookie比如一个登录后的sessionid只要你不退出它在浏览器生命周期内基本不变爬虫直接复用即可。而动态Cookie是网站前端JavaScript代码在运行时根据当前时间、用户操作、甚至是页面上的某些元素值实时计算生成的一个或多个Cookie值。服务器端会校验这个Cookie的合法性如果校验不通过就直接拒绝你的请求。这种机制的核心目的就是区分“真人操作的浏览器”和“机械的爬虫脚本”。我最近在分析几个数据平台时就频繁撞上这堵“动态Cookie墙”。它们看起来是普通的网页但核心数据接口的请求必须携带一个由前端JS生成的、有时效性的sign或token参数通常设置在Cookie或请求头里。直接抓包复制下来的值过几十秒就废了。这就是一场典型的“JS Cookie反爬”攻防战。对于爬虫开发者来说攻克它意味着你需要暂时放下requests库深入浏览器的世界去理解并重现那段生成Cookie的JavaScript逻辑。这个过程我们称之为“JS逆向”。2. 核心思路逆向工程与逻辑复现面对JS动态Cookie硬着头皮去频繁更换手动抓取的Cookie是徒劳的。我们的核心思路非常明确找到生成目标Cookie的那段JavaScript代码理解其算法然后用Python或其他语言实现相同的算法从而在爬虫程序中实时生成有效的Cookie。这个思路可以拆解为几个关键步骤它们构成了一个标准的逆向分析流程2.1 目标定位找到关键请求与Cookie首先你需要明确你的爬虫目标是什么数据。用浏览器推荐Chrome或Edge的开发者工具正常访问目标页面打开Network网络面板并勾选Preserve log保留日志。触发数据加载进行能触发你想要数据加载的操作比如点击“下一页”、筛选条件、或滚动加载。筛选请求在纷杂的网络请求中通过类型通常是XHR或Fetch和URL关键词如api,data,list等快速定位到那个返回了目标数据的请求。检查请求头点击这个请求查看它的Headers。在Request Headers请求头里仔细查找Cookie字段。你需要确认这个请求携带的Cookie中哪些是固定的登录凭证如sessionid哪些是看起来像随机字符串、可能动态变化的如signabc123def456、tokenxyz789、acw_sc__v2...等。这个动态的部分就是我们的主攻目标。2.2 代码追踪锁定生成逻辑定位到关键Cookie后下一步就是找到它在哪生成的。这里有几个核心的追踪技巧全局搜索在Sources源代码面板按CtrlShiftF打开全局搜索。直接搜索目标Cookie的名称比如sign、acw_sc__v2。你可能会找到多处引用优先关注赋值语句如document.cookie signxxx或将其作为参数传递给setRequestHeader的代码。断点调试这是最有效的手段。在Network面板中对那个关键请求右键选择“Copy - Copy as cURL”然后在命令行执行可能很复杂。更直接的是在Sources面板找到请求发起处的JS文件通常可以在Initiator调用栈里看到在可能设置Cookie或相关参数的地方打上XHR/Fetch断点或行断点。重新触发请求代码执行就会在此暂停。调用栈分析当代码在断点处暂停时查看Call Stack调用栈。这个栈从上到下展示了当前函数被谁调用。一层层往下看你往往能找到最源头的数据生成函数。这个函数里通常包含了计算逻辑。2.3 逻辑分析理解算法与依赖找到疑似生成函数后不要急着复制代码。你需要静下心来分析输入是什么函数接收哪些参数可能是当前时间戳Date.now()、页面的某个全局变量、一个固定字符串、甚至是鼠标移动轨迹的哈希值。核心算法是什么函数内部做了什么常见的操作包括字符串拼接、MD5/SHA1/SHA256等哈希运算、Base64编码/解码、AES/DES/RSA加密解密、以及各种位运算。依赖了哪些环境变量JS代码有时会依赖浏览器特有的对象如window、document、navigator或者一些由其他JS文件定义的全局变量。这些都需要在Python中模拟或计算出来。注意网站开发者可能会对JavaScript代码进行“混淆”处理将变量名、函数名替换成无意义的短字符如_0x12a4b并压缩代码结构增加阅读难度。这时需要耐心结合断点调试观察变量的具体值来反推混淆后代码的真实逻辑。2.4 代码复现用Python重写算法理解了算法逻辑后就可以用Python进行复现了。这一步的关键是等价转换。翻译运算将JS的字符串拼接、Math对象下的函数如Math.floor,Math.random、各种位运算符^,,|,,用Python的对应语法实现。特别注意JS的是无符号右移而Python的是带符号右移需要特殊处理。引入加密库对于哈希、加密操作Python通常使用hashlibMD5, SHA、hmac、base64、Crypto或cryptography等库来实现。确保使用的算法、编码方式和密钥与JS端完全一致。模拟浏览器环境如果JS算法依赖window.btoaBase64编码、window.atob解码可以用Python的base64.b64encode/decode模拟。如果依赖一个由其他JS生成的复杂全局对象你可能需要追溯并复现那个对象的生成过程或者更简单地尝试用requests或PyExecJS/js2py直接执行那部分JS代码来获取值。3. 实战案例拆解一个典型的Sign参数逆向理论说再多不如一个实例。假设我们目标网站的一个数据接口必须在Cookie中携带一个名为_signature的参数才能访问。通过抓包发现这个值每隔几分钟就会变。3.1 定位与初步分析通过浏览器开发者工具的网络面板我们找到了数据请求GET https://api.target-site.com/data/list。在其请求头的Cookie中除了常规的session确实有一个_signature7c83f8a9e2b1d045c6f7...。在Sources面板全局搜索_signature我们找到了一段高度混淆的代码。经过格式化后关键部分如下function getSign(t) { var e Date.now().toString(); var n md5(e SALT_STRING t).substring(8, 24); return btoa(n | e); }同时在发起请求的代码附近我们看到var pageId window.globalPageId || default; var sign getSign(pageId); xhr.setRequestHeader(Cookie, _signature${sign}; ${existingCookie});3.2 算法解读现在逻辑清晰了getSign函数接收一个参数t这个t来自window.globalPageId如果不存在则用default。我们需要知道这个pageId是什么。函数内部 a. 获取当前时间戳毫秒并转为字符串e。 b. 将e、一个固定的盐值SALT_STRING和参数t拼接起来。 c. 对这个拼接后的字符串进行MD5哈希。 d. 取MD5结果字符串的第8到24位共16个字符。 e. 将这16个字符与时间戳e用竖线|连接。 f. 对整个连接后的字符串进行Base64编码。 g. 最终结果作为_signature的值。3.3 Python复现代码根据以上分析我们可以用Python完美复现这个算法import hashlib import base64 import time def generate_signature(page_iddefault): 根据JS算法生成 _signature :param page_id: 页面ID对应JS中的 window.globalPageId 或 default :return: 计算得到的 _signature 字符串 # 1. 获取当前时间戳毫秒并转为字符串 timestamp_ms int(time.time() * 1000) e str(timestamp_ms) # 2. 拼接字符串时间戳 盐值 page_id # 注意JS中是 e SALT_STRING t顺序和内容必须完全一致 raw_string e SALT_STRING page_id # 3. 计算MD5哈希 md5_hash hashlib.md5(raw_string.encode(utf-8)).hexdigest() # 4. 截取第8到24位字符Python字符串索引从0开始切片是左闭右开 # JS的 substring(8,24) 对应Python的 [8:24] n md5_hash[8:24] # 5. 拼接 n 和 e中间用竖线分隔 combined n | e # 6. 进行Base64编码。JS的btoa默认对二进制字符串进行编码。 # 我们需要先将字符串转为bytes然后base64编码最后解码为字符串去掉末尾的\n signature base64.b64encode(combined.encode(utf-8)).decode(utf-8) return signature # 使用示例 if __name__ __main__: # 假设我们从页面HTML或之前的请求中提取到了 pageId page_id homepage_123 current_sign generate_signature(page_id) print(f生成的 _signature: {current_sign}) # 将其添加到Cookie中 cookies { session: your_session_value, _signature: current_sign } # 然后使用requests发起请求...3.4 关键细节与避坑指南字符串编码一致性这是最容易出错的地方。JS和Python的字符串编码必须一致。在计算MD5前确保拼接的字符串编码一致通常都是UTF-8。hashlib.md5()需要传入bytes对象所以要用.encode(utf-8)。MD5输出格式hashlib.md5().hexdigest()返回的是32位十六进制字符串小写这与JS中常见的md5库输出一致。确保你复现的JS代码使用的MD5库输出也是十六进制字符串而不是Base64或其他格式。Base64编码细节Python的base64.b64encode()返回的是bytes需要.decode(utf-8)转为字符串。JS的btoa可能不会包含换行符而Python的b64encode默认也不会所以通常没问题。但如果遇到特殊字符集可能需要关注。时间戳的同步性服务器的时间可能和你的本地时间有微小偏差。如果签名包含时间戳且服务器校验非常严格例如只允许±5秒内的请求你可能需要先请求一个服务器时间接口来校准或者将本地时间稍微提前一点。page_id的来源在这个案例中page_id(window.globalPageId) 是一个变量。你需要弄清楚它是如何生成的。它可能写在页面的某个script标签的变量里也可能来自上一个请求的响应体。你需要用爬虫先获取这个值再代入计算。4. 进阶挑战复杂环境与混淆代码上面的案例是清晰且未混淆的。实战中情况往往更复杂。4.1 处理代码混淆混淆代码通常将所有变量和函数名替换为短标识符并可能加入无用的代码块。面对混淆代码依赖调试器而非肉眼不要试图完全读懂混淆后的代码。在疑似生成Cookie的函数入口打上断点。观察输入输出在调试器中查看函数调用时传入的参数值以及函数返回的结果。记录多组输入输出尝试寻找数学关系。使用“Hook”技术在Console中你可以重写Hook关键函数。例如在代码执行前注入var _old_btoa window.btoa; window.btoa function(data) { console.trace(btoa called with:, data); var result _old_btoa(data); console.log(btoa result:, result); return result; }这样每次调用btoa时你都能在控制台看到它的输入和输出极大简化了分析过程。类似地可以HooksetRequestHeader来查看所有被设置的请求头。4.2 处理环境依赖有些JS代码严重依赖浏览器环境例如window对象属性如window.location.href,window.navigator.userAgent。在Python中你需要手动构造这些值。User-Agent必须和你的爬虫请求头里设置的一致。浏览器特有的API如Canvas用于生成图形验证码的指纹。如果Cookie生成依赖Canvas绘图结果复现将极其困难。这时可以考虑使用无头浏览器如playwright、selenium来直接执行JS获取Cookie但会牺牲效率。其他JS文件定义的全局变量你需要找到这个变量是在哪个文件、如何被赋值的。可能它本身也是通过一个复杂计算得到的。如果计算不依赖DOM等浏览器环境可以尝试用PyExecJS库在Python中执行那段JS代码来获取值。4.3 无头浏览器方案最后的武器当逆向分析成本过高或者动态Cookie的生成逻辑与鼠标移动、页面渲染等强交互行为绑定时使用无头浏览器是务实的选择。以playwright为例from playwright.sync_api import sync_playwright def get_cookie_by_browser(url): with sync_playwright() as p: # 启动浏览器推荐chromium可配置为无头模式 browser p.chromium.launch(headlessTrue) context browser.new_context( user_agent你的浏览器UA, viewport{width: 1920, height: 1080} ) page context.new_page() # 导航到目标页面 page.goto(url) # 可能需要等待页面JS执行完毕或触发某些动作 page.wait_for_timeout(2000) # 等待2秒 # 或者 page.click(button#load-data) # 从浏览器上下文中获取完整的Cookie cookies context.cookies() browser.close() # 将cookies列表转换为requests可用的字典格式 cookie_dict {cookie[name]: cookie[value] for cookie in cookies} return cookie_dict注意事项效率无头浏览器启动和页面加载远慢于纯HTTP请求仅适用于低频或登录复杂度极高的场景。资源注意关闭浏览器避免内存泄漏。指纹高级反爬会检测无头浏览器特征。playwright和selenium可以通过额外参数进行一些伪装但并非万能。5. 工具链与调试技巧工欲善其事必先利其器。一套顺手的工具能极大提升逆向效率。5.1 浏览器开发者工具进阶用法条件断点右键点击行号选择“Add conditional breakpoint”可以设置一个条件只有当条件为真时才会中断。例如在循环生成Cookie的地方可以设置条件只在变量包含特定字符串时断住。XHR/Fetch 断点在Sources面板的XHR Breakpoints区域可以添加一个URL包含特定字符串的断点。任何发起匹配该URL的请求时都会在发起前暂停让你直接跳到设置请求头的代码处。本地代码替换Overrides在Sources面板的Overrides标签下选择一个本地文件夹。然后将网络上的JS文件保存到该文件夹并对其进行修改如格式化、删除混淆代码、添加调试日志。刷新页面浏览器会加载你本地修改后的版本方便调试。5.2 辅助分析工具PyExecJS / js2py允许在Python中执行JavaScript代码块。对于独立的、不依赖浏览器环境的加密函数可以将其抠出来用这些库在Python中直接调用避免手动翻译出错。import execjs # 假设我们抠出了JS函数保存为 sign.js with open(sign.js, r, encodingutf-8) as f: js_code f.read() ctx execjs.compile(js_code) signature ctx.call(getSign, page123) print(signature)CryptoJS 模拟很多网站使用CryptoJS库进行加密。你可以直接引入CryptoJS的Node.js版本或网页版到PyExecJS环境中或者寻找Python下对应的实现如pycryptodome。AST抽象语法树解析工具对于想深入研究代码混淆和还原的可以使用esprima、ast等库对JS代码进行语法树分析自动化地进行一些反混淆操作但这属于高阶技能。5.3 常见问题排查清单在复现过程中你生成的签名可能仍然无效。请按以下清单排查问题现象可能原因排查步骤签名长度/格式不符编码或截取出错1. 逐行打印Python每一步的结果与JS调试器中每一步的结果对比。2. 检查Base64编码后的字符串是否包含换行符(\n)是否需要replace(‘\n’, ‘’)。3. 检查MD5输出是32位十六进制还是Base64。服务器响应“签名过期”时间戳不同步或算法中时间单位错误1. 检查JS用的是Date.now()毫秒还是Math.floor(Date.now()/1000)秒。2. 请求一个服务器时间接口计算本地与服务器的偏移量。3. 在生成签名的时间戳上增加一个小的正向偏移如500ms试试。签名完全无效但算法看似正确依赖了未发现的隐藏参数或全局状态1. 在JS生成签名的地方用console.log打印出函数内部所有中间变量的值。2. 检查函数是否依赖了未传入的上级作用域变量。3. 是否在每次请求前页面执行了其他初始化代码改变了某个全局变量只有第一次请求成功Cookie有使用次数限制或与Session绑定1. 检查响应头是否返回了新的Cookie需要更新。2. 是否每次请求都需要重新调用一个初始化接口来获取新的“盐”或“密钥”3. 尝试为每个请求都重新运行一次完整的签名生成流程。6. 经验总结与安全边界经过多次实战我总结出几条核心经验逆向第一原则结果比对优先。不要一头扎进复杂的混淆代码里。先Hook关键函数或打上断点收集多组“输入参数-输出签名”的对应关系。如果能通过黑盒测试猜出算法比如发现只是简单的MD5(时间戳)那就不用去分析代码了。保持环境一致性。你的爬虫请求头特别是User-Agent、Accept-Language、Referer等要尽量模拟你调试用的那个浏览器。有些签名算法会偷偷地从navigator对象里取一些信息参与计算。理解业务逻辑。动态Cookie反爬往往和业务逻辑相关。比如一个查询接口的签名可能包含了查询参数。一个分页接口的签名可能包含了页码。多思考“这个参数为什么在这里”能帮你更快定位关键代码。最后必须强调安全与合规的边界。我们研究JS反爬技术目的是为了学习Web安全知识、理解前端与后端交互的复杂性或是为了对自家网站进行安全加固。切勿将其用于未经授权地爬取受法律保护或明确声明禁止爬取的数据。侵犯个人隐私。对目标网站进行恶意攻击如高频请求导致服务器瘫痪。绕过付费墙获取本应付费的内容。技术的刀刃可以面向难题但刀柄必须握在合规的手中。在开始任何爬虫项目前请务必阅读网站的robots.txt协议和相关服务条款尊重网站所有者的意愿和资源负载。