爬虫签名机制与JavaScript逆向:从请求头到Python算法复刻

发布时间:2026/10/17 1:40:13
爬虫签名机制与JavaScript逆向:从请求头到Python算法复刻 简介针对小红书平台数据采集的Python算法还原项目面向爬虫开发者与JavaScript逆向学习者核心目标是还原x-s、x-common签名参数的生成逻辑并对平台各类请求接口进行抽象封装支持GET、POST等常见请求类型从而在不深入理解内部机制的前提下实现毫秒级、高频率数据抓取大幅降低采集门槛做到开箱即用。资源包共29个文件以11个Python脚本为核心涵盖核心算法、工具函数与测试用例同时包含YAML/TOX等持续集成配置、RST/Markdown格式的开发文档以及Makefile构建脚本整体仅28KB结构紧凑、轻量易用。目前已有4445人学习适合具备一定Python爬虫基础、希望进阶研究JavaScript逆向与反爬对抗的开发者。项目提供可直接调用的封装接口、完整的算法还原思路、单元测试与项目文档并预留了CI工作流与多环境配置既能作为小红书数据采集的即用方案也可作为学习爬虫封装与逆向工程的参考样板。1. 采集卡在签名许多爬虫翻车的地方不是网络而是算法黑匣子一份能正常跑通的数据采集脚本真正的门槛并不是网络请求怎么写而是那一串坐在请求头里的签名参数。很多开发者在写 xhs 这类平台采集时第一步就会撞上一个反直觉现象浏览器里能看到完整的数据接口用 requests 按同样的 URL 和参数构造请求返回的却是一段加密提示或直接拒绝访问。问题不在别处就在采集脚本复现了请求体却复现不了那套由 JavaScript 动态生成的签名算法。这套算法在运行时像一个黑匣子输入时间戳、设备指纹和请求路径输出一段定长字符串。只要这段字符串对不上服务端的校验规则请求就会被当作异常流量拦截。搞懂它意味着你得同时掌握 HTTP 请求的组装逻辑、JavaScript 逆向的断点定位方法以及如何在 Python 侧把这些算法完整复刻出来。这不是单纯的“爬虫JavaScript逆向”搜几个函数名就能解决的它背后是一个完整的签名机制和一套不断迭代的风控策略。2. 签名机制请求参数背后为什么要叠加多层算法2.1 从抓包看签名参数那些 x 开头的请求头是拼出来的打开浏览器开发者工具过滤出 xhs 的列表接口把请求头逐条展开会看到一串以 x 开头的自定义字段。常见的一组是时间戳、签名值、设备标识和平台编号。关键点在于这些字段不是一次性写死在请求里的而是由页面里的 JavaScript 在该次请求发出前临时计算出来的。时间戳字段的语义是“当前时间的毫秒数”用于告诉服务端本次请求的发生时刻。签名值则由一个较长的算法链生成通常包含对请求路径、请求方法、时间戳和设备信息的摘要计算。设备标识则来自浏览器本地存储或 cookie是服务端用来关联访问者的维度之一。三者缺一不可且相互绑定换个时间戳签名值就失效改个请求路径签名值也对不上。这里值得注意的一个设计动机是服务端并不希望每个请求都重新走一遍完整鉴权流程。它用签名来做轻量级的“一次性校验”只要客户端提供的签名能通过固定算法的验算就默认这个请求来自真实客户端。这也解释了为什么纯 requests 模拟请求总会失败——你伪造得了参数但伪造不了那一层由算法动态生成的“凭证”。2.2 签名生成的通用链路参数排序、序列化、加密与拼接在拿到一份算法还原后的源码后会发现签名生成通常遵循一条固定链路。先收集参与签名的原始字段把它们按字典序排序然后按字段名和值拼接成字符串接着对拼接结果做一次摘要计算最后把摘要结果与某个固定盐值或密钥再做一次组合运算得到最终签名。每一步都有讲究不能跳步。def build_x_s(payload: dict, timestamp_ms: str, secret_key: str) - str: # 1. 过滤空值并按键名排序 items sorted((k, str(v)) for k, v in payload.items() if v is not None) # 2. 拼接成 keyvaluekeyvalue 的形式 raw .join(f{k}{v} for k, v in items) # 3. 加入时间戳与路径信息 raw f{raw}ts{timestamp_ms} # 4. 用固定密钥做一次 HMAC 摘要 return hmac.new(secret_key.encode(), raw.encode(), hashlib.sha256).hexdigest()这段代码还原了签名生成的主流程参与签名的字段要先排好序时间戳作为额外字段追加在尾部最后用密钥做 HMAC 摘要。这个过程中最容易翻车的地方在于拼接格式键值之间是用等号还是冒号字段之间是用与号还是无分隔符服务端实际上是严格匹配的。还原成 Python 时必须拿着抓包得到的真实请求头接口请求头逐一比对少一个分隔符都会导致签名校验失败。常见的实现思路里密钥通常不是明文暴露的而是被藏在一个经过混淆的 JavaScript 对象中或者经过若干次异或、数组取值的运算才拿到。算法还原的工作量大部分耗在从混淆代码里找回这把隐藏密钥以及确认它的拼接顺序。2.3 多重摘要的目的让签名不被轻易伪造如果只是简单做一次 MD5攻击者对着一份请求日志就能推算出规律。因此实际采集目标上的签名机制往往在一个摘要之上再叠加一层摘要甚至穿插位移与异或操作。这种多层设计的目标不是让算法“不能被破解”而是让破解成本抬升到超过采集收益。一个典型的双层签名流程是先对请求体做 SHA256 得到第一段摘要再用第一段摘要与时间戳组合做第二次 HMAC 得到最终签名。第一层负责“锁定请求内容”第二层负责“锁定时间窗口”。服务端验签时会重算一遍完整链路同时比对时间戳是否在合理区间内超时超过几十秒的请求即便签名正确也会被拒绝。这种设计思路在风控里叫“时效绑定”。它保证了同一个签名不能在一小时后再重放因为时间戳变了之后第二层 HMAC 的输入也随之改变。这也是为什么在采集 Python 脚本里每次请求都必须重新计算签名缓存签名只会让后续请求全部失效。3. JavaScript 逆向路线从定位加密入口到用 Python 复刻3.1 断点定位三步搜索关键字、调用栈回溯、日志补点拿到一份采集目标的前端脚本后第一步不是通读代码而是精准定位签名生成位置。做法是在开发者工具里搜索签名参数的名字比如搜索“x-s”或“x-legacy-id”找到它被赋值的位置。如果源码经过了压缩和混淆搜索会命中一堆压缩后的变量名这时需要先开启格式化把单行代码展开成可读的多行再搜索字符串特征。找到赋值语句后在那一行打上断点刷新页面触发一次真实请求。断点命中时调用栈会告诉你是哪个函数调用了这段赋值逻辑。从调用栈往上回溯两层通常能看到一个计算签名结果的函数它接收请求体和时间戳作为参数返回值直接赋给请求头。此时需要做一次日志补点在计算函数入口处通过控制台打印入参和中间结果确认输入输出的对应关系。// 在计算函数入口处插入日志观察参与签名的原始字段 function generateSign(params, timestamp) { console.log([params], JSON.stringify(params)); console.log([timestamp], timestamp); const raw buildRawString(params, timestamp); console.log([raw], raw); const sign hmacDigest(raw, SECRET_KEY); console.log([sign], sign); return sign; }这段日志代码的意义在于把黑匣子变成白盒子通过打印入参、拼接串和最终签名逆向人员可以确认服务端期望的拼接格式到底长什么样。如果从断点中看到原始字符串是“a1b2ts1700000000000”这种格式那么 Python 侧就必须严格复现这种顺序。3.2 AST 反混淆把压缩和编码后的表达式还原成可读逻辑大规模前端脚本常见的保护手段是代码压缩与标识符混淆。压缩只去除空格和缩短变量名逻辑结构不变混淆则会把函数名改成无意义字符甚至把字符串拆成数组运行时再通过索引取回。对于后者直接阅读源码效率很低需要借助 AST 工具做一次自动还原。import subprocess import json def prettify_js(js_code: str) - str: # 用 Node 的 babel 解析混淆代码去除无用节点 proc subprocess.run( [node, -e, const babelrequire(babel/core); const codefs.readFileSync(0,utf8); const outbabel.transformSync(code,{compact:false,minified:false}); process.stdout.write(out.code);], inputjs_code, capture_outputTrue, textTrue ) return proc.stdoutAST 反混淆的思路是先把源码解析成抽象语法树然后遍历节点简化冗余表达式、还原字符串拼接、展开被调用的函数。这一步做完后原本面目全非的函数体恢复出基本结构你就能看出签名算法用的是 HMAC 还是普通摘要。以常见实现来说整个还原流程的耗时占比中AST 预处理占到一半以上真正的算法识别反而很快。核心要点是不要把 AST 当作万能工具。它只负责恢复结构不负责解释逻辑。遇到多层嵌套的立即执行函数还需要配合运行时日志才能确认哪一段代码实际执行哪一段只是干扰项。实践中的做法是“AST 粗读 运行时精调”先用工具把整体结构理顺再靠断点和日志把关键数据流打通。3.3 补环境与 RPC两种主流的签名复用风格还原出算法逻辑后落地到 Python 侧有两条路一条是把算法完整改写为 Python 实现另一条是用 Node 环境直接执行原版 JavaScriptPython 通过子进程调用取签名结果。前者在算法明确时最干净后者在全量还原代价高时更省事。const vm require(vm); const sandBox { window: {}, navigator: { userAgent: Mozilla/5.0 }, location: { href: https://platform.example.com }, console, }; vm.createContext(sandBox); // 把抓取到的签名函数源码放到 context 里执行 const scriptCode function getSign(args) { // 原版混淆函数体直接使用 sandbox 中的对象 return args.join(,); } ; vm.runInContext(scriptCode, sandBox); module.exports function getSignFromSandbox(args) { return vm.runInContext(getSign(${JSON.stringify(args)}), sandBox); };vm 补环境的思路是创建一个最小的浏览器环境沙箱把原版脚本丢进去运行。它模拟了 window、navigator、location 等对象避免脚本因为缺少宿主对象而报错。这种做法的好处是不用理解内部算法坏处是需要持续维护环境模拟代码一旦原版脚本引用了新的宿主方法补环境代码也要同步更新。RPC 桥接则是另一种取舍让本机浏览器或一个无头浏览器真正执行脚本Python 通过网络或进程通信请求签名结果。它的稳定度最高因为它运行的就是原版环境但代价是每获取一次签名可能产生一次真实浏览器初始化耗时。在设计上多数采集方案会优先尝试完整算法还原因为纯 Python 实现的请求吞吐量远高于桥接方式只有还原到“还差最后一层参数搞不定”时才会把补环境作为兜底方案。4. 算法落地避坑风控边界比算法本身更值得研究4.1 高频采集触发风控签名对了也能返回加密提示现象签名算法还原后请求能正常返回数据但连续采集几分钟后某个接口突然返回一段不可读的加密提示或直接要求完成滑块验证。原因签名通过只代表“请求合法”不代表“行为正常”。服务端还会按单位时间内的请求量、同一设备标识的活跃度、请求间隔的方差做统计。一段固定间隔的定时请求在风控模型里是很明显的特征因为真人操作永远有抖动的思考时间。解决把请求间隔改为随机区间比如 3 到 7 秒之间取随机值限制单设备标识下的并发数在连续请求之间穿插一次页面访问或滚动动作让行为曲线更接近真实用户。核心原则是降低单位时间内的请求密度而不是试图绕过验证码机制。把采集看作对公开内容的低速读取而不是对接口的高频压测这样才能让签名算法的还原成果持续生效。4.2 签名版本迭代算法升级时的排查路径现象某天启动采集脚本所有请求都返回签名错误即使重新抓包对比请求头也看不出明显差异。原因服务端更新了签名算法但旧算法生成的签名还在缓存期内看起来像“没变”或者新算法在拼接格式里多加了一个字段而抓包时这个字段因为值为空而被忽略了。解决不要盲目重新跑一遍逆向流程。先对比旧请求和新请求的请求头字段差异然后回到 JS 搜索签名赋值处的调用栈看新增了什么取值逻辑。常见的是时间戳单位从秒变成毫秒或者请求头里增加了一个随机数参与摘要计算。定位到差异后只需要在 Python 侧补上对应字段即可通常不需要重写整个算法流程。4.3 设备标识维度被追踪只关注签名忽略变量现象签名算法正确单机单 IP 采集正常换到服务器环境后请求频繁失败即便是低频率也一样。原因服务器数据中心的 IP 段和浏览器指纹特征与普通用户差异明显。服务端除了验签还会比对该 IP 段的历史行为。同一个 IP 段下大量短连接请求会被直接归入风险类别。解决把设备标识视为资源而非固定常量。采集前先构造一批随机但合法的设备标识序列每次请求随机选用一个同时控制同一设备标识的请求间隔。IP 维度的处理要遵循目标平台的服务条款只对公开数据做合理低速读取不做并发穿透类操作。4.4 请求头字段缺失抓包数据不完整导致的假阳性现象用例测试中签名校验通过了但正式运行时数据量总少一截或者部分接口偶发返回空数据。原因抓包时浏览器补全了某些默认请求头比如 accept-language、referer、origin而 Python 脚本只保留了抓包中明显可见的 x 开头字段。服务端对缺失字段的请求执行了宽松校验过滤掉了部分返回值。解决在采集脚本中把请求头补齐到与浏览器一致。用浏览器的“复制为 cURL”功能导出完整请求头然后逐项比对 Python 脚本中的 headers 字典。多数的翻车都发生在 accept-language 或 sec-ch-ua 这类默认字段上而不是签名本身。5. 设计取舍用成本最低的路径维持采集稳定性5.1 完整算法还原与 RPC 桥接的成本对比两类实现路径在工程上各有利弊。完整算法还原适合长期采集因为它不需要额外进程Python 内部直接计算签名请求吞吐量和稳定性都可控但它要求逆向工作做到“最后一公里”任何一处拼接细节出错都会让全部工作量归零。RPC 桥接则适合初期验证或算法不明朗的阶段它把计算复杂度转移到真实浏览器里即使不懂内部逻辑也能跑通请求代价是每次签名计算都多一次进程间通信请求速度明显慢。对比维度完整算法还原RPC 桥接计算方式Python 原生实现Node/浏览器执行原版 JS请求性能高无额外进程开销低受桥接通信耗时影响维护成本算法变更时需重新定位需同步维护补环境或桥接服务上手门槛需要掌握 JS 逆向熟悉子进程调用即可我一般会建议先跑通 RPC 桥接确认数据可用性再逐步把算法替换为 Python 实现。这样既不会因为算法还原的延误而拖垮项目又能给逆向留出充足时间。如果目标是长期稳定的数据通道最终一定要落在完整算法还原这条路上因为桥接方式在并发场景下的瓶颈会很明显。5.2 验证当前实现是否健康的三个信号验证采集实现是否被风控盯上看三个信号就够了。第一个是返回数据条数曲线正常情况应该稳定在接口返回上限附近如果偶发返回一半或不足上限说明服务端在降级响应。第二个是错误码分布持续观察一段时间的状态码如果某些特定状态码开始零星出现说明行为模型正在调整。第三个是签名校验失败率它不应该有突然跳变的台阶正常的失败率在低个位数如果一夜之间升到两位数说明算法版本已经更新。这三个信号可以写进采集进程的日志里作为健康度指标定期输出。一旦发现降级优先降低采集频率而不是加大并发去试探多数限流是动态的把频率降下来之后风险等级会自动回退。5.3 一段强制验证习惯从我开始做采集相关项目起每次改完算法逻辑都会强制走一遍“旧请求回放”的验证流程把抓包保存的原始请求重新发出三遍确认返回一致再用新算法生成的签名替换其中一次确认服务端接受。这一步看起来笨却能在上线前拦截掉大部分签名拼接细节错误。从那以后数据采集脚本的翻车率明显下降。做一个复杂算法还原时最不值得踩的就是“明明签名格式只差一个冒号却花了两天去查”。希望这整套签名机制与验证习惯的拆解能帮你在处理同类数据采集目标时少走一段弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询