做数据采集这几年我最怕看到的不是验证码而是请求参数里突然冒出来的sign字段。最近我处理了一个中文期刊服务平台的检索接口表面上界面干净、请求规范动手之后才发现加密参数藏在层层JS逻辑里。这个项目断断续续花了两个晚上期间踩了不少坑也把“定位加密入口、断点调试、算法还原、本地复现”这条路完整走了一遍。这篇文章就把整个逆向过程和工程化处理思路记录下来适合有一定Python爬虫基础、想系统了解JS逆向完整链路的朋友参考。1. 前期侦察先把请求链路看清楚1.1 抓包定位核心请求第一步永远是打开浏览器开发者工具把Network面板清空然后在站内搜索一个关键词。期刊服务平台的工作台和普通网站不太一样它的检索请求往往不是页面刷新而是通过XHR异步拉取数据。输入“人工智能”点检索按钮XHR列表里会多出几条记录其中一条返回的是标准的JSON结构里面的字段基本就是我们要的数据源。点开这条请求重点看两部分Request URL里的查询参数以及Request Headers里的自定义请求头。我遇到的这个平台查询参数非常规整有keyword、page、size、sort看起来都是明文但末尾多了两个字段sign和token。token在请求头里还有一个副本两者大概率是服务端校验身份和签名用的。这里有一个重要习惯不要急着看JS文件先把请求全貌截图保存。因为后面还原算法时需要反复对比参数格式。我还顺手记下了响应体里的总条数和分页信息这些数据用于验证后续抓到的数据是否完整。1.2 识别加密参数的特征拿到sign和token之后先做静态观察。请求里的sign看起来像32位的十六进制字符串大小写混合这个形态很像MD5。但单凭长度和字符集不能下定论也可能是SHA1截断或其他自定义哈希。token则是一串Base64字符末尾带等号一般是某段用户态信息的编码。我习惯用“删参数法”来判断签名依赖哪些字段。具体操作是用浏览器的编辑重发功能把sign删掉再发送观察是否返回认证失败再把token删掉观察是提示参数缺失还是登录失效。这里我实测的结果是sign缺失直接返回400说明签名必须存在token缺失返回401说明它负责身份校验。这个差异很重要它决定了后面逆向的重点是签名算法还是Cookie链路。另外还要看sign是否随时间变化。同样的关键词隔几分钟再发一次对比两次请求的sign是否一致。如果不一致说明里面至少混入了时间戳如果一致说明只与业务参数有关复现起来会省事很多。这个平台两次请求的sign不一样基本可以确定有时间因子。1.3 技术选型还原算法还是运行时调用逆向路线一般两条一是看懂加密逻辑把算法翻译成Python二是不管内部实现直接在浏览器环境里把原函数调用起来。两种方案各有适用场景。这次我选择的策略是先尝试算法还原。判断依据有三点第一这个平台的JS文件经过压缩但格式还很规整没有看到明显的变量名混淆可读性尚可第二sign的形态是短哈希逻辑理论上不会太复杂第三项目中后期需要大量并发请求纯Python重写性能更好也不用为Node/Puppeteer额外维护一套服务。当然如果测试过程中发现算法极度复杂比如多层AES嵌套、动态密钥、每次请求都会变化盐值我会果断切换成运行时调用方案。这个决策点需要尽早判断否则容易在还原算法上消耗太多时间。评论区经常有朋友问我“为什么我的逆向做不下去”十有八九是选错了路线死磕一个本可以绕过的环节。2. 逆向核心从函数定位到逻辑还原2.1 全局搜索定位加密入口定位加密代码的常规套路是全局搜索关键词。在开发者工具的Sources面板里按CtrlShiftF打开全局搜索输入sign回车。搜索结果会列出所有包含这个字符串的文件和行号。不过这里有一个坑压缩后的JS文件通常只有一行搜索结果里显示的行号参考意义不大需要点击文件进去再点击左下角的格式化按钮把代码展开成多行。格式化之后的代码依然很长可能几千行这时候不要从上往下读直接用搜索框定位到sign出现的位置。我定位到的文件是一个体积不小的通用工具包sign在里面出现了多次分别负责参数名拼接、签名计算和值回填。真正的加密函数在文件后半段命名为encryptRequestParam它接收一个包含所有业务参数的字典返回处理后的对象。这类函数名一般是开发人员手写的没有做混淆对逆向者非常友好。如果遇到变量名全是_0x开头的情况就是OB混淆了需要额外走AST还原的路线我后面会说。2.2 断点调试与调用栈回溯在encryptRequestParam函数的入口处打一个断点然后回到页面重新触发检索请求。执行会停在断点位置此时右边的Scope区域可以看到所有局部变量其中就有传入的原始参数字典。接下来单步执行重点观察三样东西一是参数字典里新增了哪些字段二是时间戳从哪里取三是加盐拼接操作的具体位置。我在这个平台看到的是页面先取了一个13位毫秒时间戳截取前10位作为ts然后对业务参数做字典序排序拼接成keyword人工智能page1这样的字符串末尾追加一段固定盐值整体丢进一个自定义哈希函数。这里有一个实用技巧在可疑的return语句上打条件断点。比如想观察某个变量是否等于特定值可以在断点编辑框里写ts.length 10命中才暂停。对于循环内反复执行的代码条件断点能省下大量无意义的单步操作。另外Call Stack面板会显示函数调用链从encryptRequestParam往上找两级通常能看到它被哪个接口方法调用这有助于确认签名覆盖了哪些请求头字段。调用栈回溯还有一个隐蔽用法如果断点命中时 Scope 里看不到某个关键变量可以去 Call Stack 里点击上层函数帧Scope面板会切换到那个帧的上下文。这个操作能帮我们找到变量最早来源而不是只看到加密函数内部手工拼出来的中间量。2.3 常见混淆与应对思路这个平台的情况算简单的但很多期刊类平台的老系统会用到OB混淆或者字符串数组化来保护前端代码。如果遇到这类情况有两条路可以走。第一条是借助AST工具还原。OB混淆的本质是把函数名、变量名替换成_0x格式并把字符串常量抽离到一个数组里按索引引用。把这份代码丢给AST解析库写一个插件把数组还原、把索引引用替换成实际字符串、把标识符改回可读命名代码可读性会大幅提升。市面上的开源还原工具可以做到80%以上的还原度剩下的手调即可不需要完全搞懂混淆器的每一种变换。第二条是绕开代码可读性直接做黑盒调用。如果目标JS已经被极度混淆还原源码的时间成本可能超过写爬虫本身不如把整个文件原封不动放进Node环境找出入口函数调用它。这个方法的核心是解决浏览器API依赖也就是下面要说的补环境。这两条路我都在不同项目里走过整体上“先花半小时评估混淆深度再决定是否还原”是比较稳妥的节奏。3. 加密还原与本地复现3.1 核心算法还原断点调试看到完整逻辑后我整理出的签名流程大致如下// 原始JS逻辑经过格式化和注释整理 function encryptRequestParam(params) { var ts Math.floor(Date.now() / 1000).toString(); params.ts ts; var keys Object.keys(params).sort(); var str ; for (var i 0; i keys.length; i) { str keys[i] params[keys[i]] ; } str str.slice(0, -1); str secret_key_placeholder; var sign customHash(str); params.sign sign; return params; }还原到Python时我写成了这样import hashlib import time from urllib.parse import quote def make_sign(params: dict, secret_key: str) - str: params[ts] str(int(time.time())) ordered_keys sorted(params.keys()) raw .join(f{k}{params[k]} for k in ordered_keys) raw secret_key return hashlib.md5(raw.encode(utf-8)).hexdigest()这里有个细节平台原生的customHash内部对拼接后的字符串做了两次MD5迭代第一次得到32位哈希第二次把第一次的哈希结果混入时间戳再次计算。翻译成Python时不能只做一次MD5前后要完全对齐。验证方法很简单把浏览器里看到的sign与本地计算值对比连续跑通三条不同关键词请求后再确认无误。3.2 补环境与mock方案签名算法还原之后我发现token的生成没有这么简单它需要从document.cookie中取一个设备指纹再用RSA公钥加密。这个逻辑强依赖浏览器环境在纯Python里很难重写所以我临时切换到了补环境方案这也是很多项目里需要用到的技能。所谓补环境就是用Node.js模拟出浏览器环境下的window、document、navigator等全局对象让原本在浏览器里运行的加密JS在Node里也能不报错地执行。我用的方式是基于jsdom再手动补齐Node缺的APIconst { JSDOM } require(jsdom); const dom new JSDOM(!DOCTYPE htmlhtmlbody/body/html, { url: https://example.com, referrer: https://example.com, userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }); global.window dom.window; global.document dom.window.document; global.navigator dom.window.navigator;实际跑代码时还是不断报错。最常见的三个window.screen不存在、localStorage.getItem返回空、navigator.languages未定义。我的处理方法是给这些API逐一写mock把缺失的字段从真实浏览器的console里拷贝过来填成固定值。补环境有个原则能用固定值就用固定值不要为了追求“完全一致”去动态生成一堆指纹。服务端校验的是这个指纹是否在合法范围内而不是它每次都必须不同。实测下来固定UA、固定屏幕分辨率、固定语言列表就能稳定通过校验完全不需要去复刻一套完整的指纹算法。3.3 运行时调用与RPC方案补环境能解决一部分问题但遇到密钥动态变化、服务端验签逻辑升级快的场景老老实实还原还是太慢。我在这个项目里最后采用的是一个混合方案核心接口参数在Python里生成token通过一个本地RPC服务从浏览器环境实时获取。具体做法是启动一个无头浏览器打开平台首页通过page.exposeFunction把Node里的本地接口暴露给页面页面里的JS脚本在加载完加密模块后把这个token的生成函数挂到window上。Python端需要token时发一个HTTP请求到本地服务本地服务再通过CDP协议远程调用页面里的生成函数拿到结果后返回。这种方式的好处是不需要理解token内部算法只要浏览器能生成我们就能取到。缺点是性能不如纯算法还原每次获取有少量开销而且如果平台做了设备IP与token绑定频繁更换IP会触发风控。所以我的取舍是高频接口用Python重写主参数签名低频高价值接口用RPC兜底。4. 平台特性与配套处理4.1 期刊平台常见反爬机制对比中文学术服务平台与电商、资讯类网站相比反爬策略有自己的特点。我把常见的几类机制整理成了一张表方便后续做类似站点的朋友快速对照反爬机制常见实现方式识别难度应对思路请求签名参数排序哈希AES高定位生成函数并还原设备指纹Cookie中的指纹字段高补环境或RPC登录态校验Cookie/Ticket有效期中维护Cookie池字体反爬自定义字体映射数字中解析字体映射表频控限制IP单次请求阈值低控制抓取速率数据分片摘要内容分布加载低模拟滚动刷新这个平台最核心的防护是请求签名加设备指纹的组合。签名只覆盖业务参数不覆盖请求头反而让还原过程少了一个变量但设备指纹和登录态绑一起一旦指纹变化过大账号会被临时冻结。这是期刊平台特有的一种温和处理方式不会直接封IP而是让你抓到的数据开始失真比如某几条摘要被替换成空白。我在排查“为什么数据对不上”时顺着这个思路检查了Cookie指纹确认是指纹过期导致的问题。4.2 Cookie与登录态自动化维持很多期刊平台的检索接口可以匿名访问但详情页和下载链接需要登录态。我的做法是准备一个Cookie池至少保存三个不同登录态的Cookies切换使用。Cookie池的维护逻辑是批量检测接口每次请求都返回新的Set-Cookie本地记录覆盖如果某个Cookie出现401或redirect标记为失效停止分配通过定时任务每两小时刷新一次所有Cookie的活跃度。这里要注意一个并发陷阱同一个Cookie同时发多个请求服务端只更新最后一次的指纹其他请求就可能被判定异常。所以一个Cookie同一时刻只允许一个线程使用用读写锁控制。登录态过期的问题很难完全避免我在项目里加了异常容错请求返回码不是200时先重试一次重试还是失败就换下一个Cookie连续失败三个Cookie则暂停任务并告警。这样即使晚上无人值守数据还能保持较高的完整率。4.3 中文检索与编码处理期刊平台历史包袱重很多接口的响应编码是GBK而不是UTF-8。如果用requests直接解析中文摘要会变成乱码。这个平台的一个详情接口就是如此第一版脚本跑完标题全是 “智能”一看就是编码没对上。处理方式很明确先检查响应头的Content-Type里有没有charset字段没有就根据HTML里的meta标签推断实在判断不出来就用二进制内容做编码探测。Python里用requests时拿到响应后手动指定编码resp requests.get(url, headersheaders) if resp.encoding is None or resp.encoding.lower() not in (utf-8, utf8): resp.encoding gbk data resp.text另一个中文相关的坑是URL编码。默认的quote()会把中文按UTF-8编码后百分号转义但这个平台的签名算法接收的原始关键词是未编码的字符串。也就是说签名用的字符串是keyword人工智能而实际请求URL里要发的是keyword%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD。这两者必须严格区分否则签名怎么都对不上。我一开始就是把编码后的值送去签了名结果服务端校验始终失败排查了好一阵。5. 常见问题与排查技巧实录5.1 排查流程与速查表做逆向久了我积累了一套自己的排查套路先确认请求源码里看到的现象是否稳定复现再逐层排查签名、Cookie、头部、编码这四个环节。下面这个速查表基本覆盖了常见的问题场景现象可能原因排查方法一直返回404接口路径拼错或需要动态路径对比浏览器全部请求路径返回400签名缺失或参数类型不正确检查签名函数是否进入返回401Cookie失效或token过期重新登录获取Cookie返回403请求头不完整或指纹异常补齐所有自定义Header返回200但数据为空检索关键词编码不符检查URL编码方式部分摘要乱码响应编码判断错误手动指定GBK解码偶发请求失败Cookie被并发复用单Cookie单线程数据与页面不一致页面混杂缓存数据清缓存或绕过CDN表格之外还有一条逻辑链如果JS逆向结果和Python还原出来的签名不一致不要直接怀疑算法先在浏览器Console里手动跑一遍原函数拿它和Python结果对比。如果原函数输出都不稳定问题可能出在页面状态如果原函数输出稳定但Python输出不同多半是数据类型转换出了问题比如数字变字符串时多了个零。5.2 高难度接口的替代方案遇到确实啃不动的接口我没有死磕逆向而是绕道走了另一条路用浏览器自动化模拟真实用户操作。比如下载PDF如果解析下载链接里的签名太复杂我就直接用自动化工具打开页面模拟点击下载流程让浏览器自己携带所有正确的签名和Cookie发起请求。这个方案的优势是完全不需要关注内部签名怎么生成劣势是速度慢、资源占用高。它比较适合量小且非高频的接口不适合大数据抓取。我的经验是用三层结构来混合高频基础数据走纯Python请求中频数据走RPC服务低频复杂操作走浏览器模拟。这样可以平衡速度和成功率。还有一类平台提供了比较规范的元数据导出接口只需要登录后输入专业领域或关键词系统会生成一批结果文件直接下载即可。这类接口往往不在主站检索流量里也没有复杂的签名校验值得在开发初期就调研是否存在。5.3 长期维护与异常监控爬虫项目做完只是第一步真正考验人的是长期稳定运行。平台的加密算法不是一成不变的我遇到过上线两周后sign从32位变成40位的情况算法格式变化后脚本立即失效。针对这个问题我养成了一个习惯把Token和签名的生成逻辑单独封装成一个模块并写一个自检函数每次运行时先请求一个已知关键词比对返回结果与预期是否一致。一旦结果异常立即触发告警而不是等到任务全部跑完才发现数据全废了。告警通道我用的最简单的Webhook把失败原因和当前请求样例发到群里手机上看一眼就知道是反爬升级还是自己的代码bug。这个项目后续维护了近两个月总共建了四条告警规则签名校验失败、Cookie过期率超标、返回数据为空、连续任务超时。每一条都对应一种真实踩过的坑没有一条是拍脑袋加的。日常还要注意请求频率控制。我是按接口维度设置的限速检索接口控制在每3秒一次详情页控制在每5秒一次下载接口进一步降低到每10秒一次。这个频率远低于阈值却能保证任务在几个小时内完成稳定性和效率都兼顾了。6. 写在最后关于工具的边界这个项目做到后期签名的还原已经变得非常熟练但我在整理方案时还是会反复提醒自己前端逆向只是技术手段它的适用范围必须是把控在自己有权限测试的范围内。比如排查自己负责的站点、验证接口参数是否符合规范、做授权范围内的数据研究这些都没问题但未经许可抓取第三方平台数据、绕过付费权益、批量获取受版权保护的文献内容既违反用户协议也存在法律风险。我个人在这个项目里只验证了公开检索接口的数据获取链路没有触碰任何账号权益相关的数据。另外给新接触JS逆向的朋友一句建议不用一上来就追求把每一条混淆代码都看明白。先学会用断点、调用栈和控制台手动执行去黑盒验证逻辑再逐步学习AST还原和补环境这条路比硬啃源码要顺畅得多。逆向工程最核心的能力是面对未知系统时知道“下一步该往哪儿试”的判断力这种能力是在一次次实际项目中磨出来的。做采集这几年我越来越体会到一件事工程化比单点技术更难。算法还原出来只是开始Cookie维护、编码处理、异常监控、频率控制任何一个环节出问题采集的数据都可能不可用。这个期刊平台项目规模不大但把整条链路走通之后我对自己处理复杂反爬站点的信心明显提升了不少。以后遇到更复杂的加密方案我心里也更有底了。
阅读完成 · 觉得有帮助?