如果你刚开始接触爬虫大概率遇到过这样的场景浏览器里明明能看到接口返回数据但用requests复制同样的 URL 去请求却拿到 401 或一串加密错误提示。问题往往出在 URL 里那几个不断变化的动态参数上。本文将围绕“爬虫逆向 JS 入门”展开完整拆解动态参数是怎么生成的以及如何从浏览器开发者工具一步步定位到生成逻辑。全文包含环境准备、断点调试、函数调用栈回溯、Python 与 Node.js 复现签名、常见报错排查、工程落地建议适合刚开始接触爬虫逆向的开发者也适合想系统理解 JS 逆向流程的后端同学。本文所有示例均使用虚构接口example.com仅用于教学演示。实际逆向任何站点前请先确认你的操作符合平台条款、相关法律法规并在授权范围内进行测试。1. 动态参数为什么你的爬虫会撞同一面墙1.1 什么是动态参数动态参数是指每次请求时都会变化、或者需要经过一段前端逻辑计算才能生成的参数。常见的有URL Query 中的sign、token、_signature、nonce、ts。Request Headers 中的动态cookie、authorization。Request Body 中被加密的字段比如data不是明文 JSON而是一串密文。与静态参数不同动态参数无法直接从 HTML 源码里复制因为服务端要求客户端必须“证明”自己是一个真实执行了前端逻辑的浏览器。1.2 为什么服务端要校验动态参数从服务端角度看动态参数主要有三个作用。第一防止请求被直接构造。如果所有参数都能在页面源码里看到攻击者或爬虫就可以轻易伪造请求。服务端通过动态签名保证请求来自携带正确前端逻辑的环境。第二防止数据被篡改。签名参数通常会把部分业务字段一起参与计算比如page1limit20如果请求方改了limit签名校验就会失败。第三便于统计与风控。动态参数里隐藏着时间戳、设备指纹、行为轨迹等信息服务端可以据此判断请求是否来自真实用户。1.3 爬虫逆向中“动态”到底指什么在实际爬虫开发中“逆向动态参数”并不是说要破解 HTTPS而是指找到前端 JS 中负责生成这个参数的函数理解它的算法、输入和输出然后用 Python、Node.js 或其他语言复现这个计算过程。这里的核心思路是浏览器能算出来的我们也可以用代码算出来。只要搞懂参数生成规则爬虫就不再依赖 Selenium 这类重型浏览器自动化工具。在开始定位之前我们需要先理解动态参数的几种常见类型否则后面看到代码时会分不清方向。2. 动态参数的常见类型与生成原理2.1 时间戳与随机数最基础的动态因素有些接口的参数名很直白比如ts1700000000000这就是毫秒级时间戳。有些接口还要求nonce或random它们是一串随机字符串通常由Math.random()或自定义随机函数生成。这类参数本身没有加密难度它的作用是让每次请求都不一样防止请求被缓存或重放。服务端校验时会判断时间戳是否在允许的时间窗口内比如前后 5 分钟过期就拒绝。// 浏览器端常见的生成方式 const ts Date.now(); const nonce Math.random().toString(36).substring(2);这种动态参数用 Python 很容易复现import time import random import string ts int(time.time() * 1000) nonce .join(random.choices(string.ascii_lowercase string.digits, k8))2.2 摘要签名参数排序拼接 散列摘要签名在爬虫逆向里非常常见也是新手必须掌握的一类。它通常把请求参数、时间戳、固定密钥等按规则拼接成一个字符串再通过 MD5、SHA1、SHA256 等散列算法生成一段不可逆的十六进制字符串。例如签名规则可能是sign MD5(params key)其中params可能是把所有参数按字典序排序后用和拼接而成。# 伪代码示例 import hashlib params { page: 1, limit: 20, ts: 1700000000000 } key a1b2c3d4 base .join(f{k}{params[k]} for k in sorted(params)) sign hashlib.md5((base key).encode()).hexdigest()这类算法本身不难难的是找到拼接顺序和密钥。密钥通常被隐藏在压缩混淆后的 JS 文件中需要通过断点调试才能拿到完整上下文。2.3 加密算法RSA / AES 混淆真实数据比摘要签名更进一步的是加密算法。摘要签名不能反解但加密算法可以把数据变成密文服务端通过私钥或密钥解密。常见场景是登录接口前端把用户名、密码通过 RSA 公钥加密后传给服务端防止密码在传输过程中被直接看到。另一个场景是请求体整体加密接口的data字段是一段 AES 密文服务端解密后再处理。RSA 是非对称加密公钥加密、私钥解密。前端 JS 里通常能找到公钥可以用jsencrypt库操作。AES 是对称加密加密和解密使用同一个密钥密钥可能写在前端代码里也可能是从某个接口动态获取的。这类内容适合放到进阶部分新手理解摘要签名后再学 RSA 和 AES 会更顺畅。2.4 Cookie 动态指纹浏览器环境参与计算有些网站会把动态参数放在 Cookie 里。常见流程是访问页面时JS 采集浏览器环境信息比如 Canvas 指纹、WebGL 信息、屏幕分辨率、UA 等然后生成一段加密字符串写入 Cookie后续接口请求带上这个 Cookie 才能通过校验。这类动态参数的特点是无法在 Python 里直接构造因为算法依赖浏览器环境对象。逆向这类参数时通常需要“补环境”也就是在 Node.js 里模拟出浏览器环境或者直接把生成 Cookie 的 JS 代码抠出来用vm模块运行。2.5 前端框架打包后的模块化代码另一个新手容易困惑的点是现代前端项目大多使用 webpack、Vite 等构建工具打包JS 代码不是一个个文件直接暴露在全局而是被封装成模块。你可能会在 Sources 面板看到类似这样的代码!function(e) { var t {}; function n(r) { if (t[r]) return t[r].exports; var o t[r] {i: r, l: !1, exports: {}}; return e[r].call(o.exports, o, o.exports, n), o.l !0, o.exports } n(0) }([function(e, t) { ... }])这种情况下动态参数生成函数不会是一个简单的全局function getSign()而是藏在某个模块内部需要通过断点回溯找到它必要时还要处理模块导出问题。第四章会具体说明定位思路。3. 环境准备与工具清单3.1 运行环境为了完成本文的示例和调试建议准备以下环境Windows / macOS / Linux 均可。Python 3.8 或更高版本。Node.js 14 或更高版本。Chrome 浏览器或 Edge 浏览器用于开发者工具调试。可选的抓包工具Fiddler、Charles、mitmproxy。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。3.2 工具与依赖在 Python 中主要用到pip install requests pip install PyExecJSPyExecJS的作用是在 Python 中调用 JavaScript 代码。它本身不是 JS 引擎需要依赖系统里的 JavaScript 运行环境。如果你安装了 Node.jsPyExecJS会自动使用 Node 作为执行引擎。在 Node.js 中主要用到内置模块cryptoMD5、SHA、AES、RSA 等加解密。vm在沙箱环境中运行 JS 代码常用于补环境。如果要执行从浏览器里抠出来的整段 JSNode.js 的vm模块是很常用的工具。const vm require(vm); const code function getSign() { return hello; } ; const sandbox {}; vm.createContext(sandbox); vm.runInContext(code, sandbox); console.log(sandbox.getSign());3.3 浏览器开发者工具准备Chrome 开发者工具DevTools是 JS 逆向最重要的战场。你需要熟悉以下面板面板作用Elements查看页面 DOM 结构有时能找到加密公钥或全局变量Console执行 JS 代码、验证函数输出Sources查看 JS 源码、打断点、单步调试Network抓取异步请求、查看请求参数和响应Application查看 Cookie、LocalStorage、SessionStorage在开始定位之前建议按F12打开开发者工具切到 Network 面板勾选 Preserve log 和 Disable cache这样刷新页面时不会丢掉请求记录。4. 动态参数的定位方法从接口回退到 JS 代码4.1 第一步用抓包锁定数据接口假设我们要抓取的页面是https://www.example.com/list页面展示了一组列表数据。打开 Network 面板刷新页面找到返回数据内容的 XHR 或 Fetch 请求。通常接口路径会包含api、list、data等字样点击请求后在 Payload 或 Query String Parameters 里就能看到动态参数。比如请求 URL 是https://api.example.com/v1/list?page1limit20ts1700000000000sign8f14e45fceea167a5a36dedd4bea2543这里ts和sign就是我们关注的对象。ts一眼能看出是时间戳sign则需要进一步分析。4.2 第二步XHR 断点定位发起位置在 Sources 面板右侧可以看到XHR/fetch Breakpoints。点击号输入接口路径关键词比如list然后刷新页面当浏览器准备发起这个请求时代码会停在发送请求的那一行。停在断点后我们要看的是右侧的Call Stack调用栈。调用栈记录了从点击事件或页面加载开始一直到发起请求的完整函数调用链条。从最上层往下一层一层看通常能找到一个包含“生成参数”逻辑的函数。这个方法非常关键因为压缩后的 JS 文件里函数名可能都是a、b、c只有在调用栈中才能看清函数之间的调用关系。4.3 第三步函数调用栈回溯假设断点停在一个类似sendRequest的函数中我们顺着调用栈往上回溯。一个典型的调用栈可能是sendRequest (index.js:100) getList (index.js:80) init (index.js:30) (anonymous) (index.js:10)此时可以先看getList函数因为它很可能负责组装请求参数。点击调用栈里的函数名会自动跳转到对应代码位置左侧能看到作用域内的变量。接着在getList里找到类似sign: getSign(参数)的代码再跳进getSign函数就能看到签名的完整算法。如果没有触发断点也可以直接在 Sources 面板里按CtrlShiftF全局搜索sign关键词定位到相关代码位置。4.4 第四步关键词搜索与格式化很多前端代码是压缩后的整个文件只有几行阅读性很差。点击 Sources 面板下方的{}按钮可以格式化代码把压缩后的代码展开成可读形式。格式化之后再使用CtrlShiftF搜索sign、token、encrypt、md5等关键词往往能找到签名函数所在的位置。需要注意的是有些站点使用了非常强的混淆字符串也可能被编码直接搜索不到关键词。这时建议优先使用 XHR 断点回溯的方式因为不管代码怎么混淆函数执行过程是真实发生的断点能带你直接找到执行现场。4.5 第五步在 Console 中验证生成逻辑当我们定位到疑似签名函数后不要急着抄代码。先在 Console 中手动调用一下验证函数输出是否与抓包里的一致。比如格式化后发现这样一个函数function getSign(params) { 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 secretKey123; return md5(str); }我们可以在 Console 中构造相同参数调用getSign看返回值是否等于请求里的sign。如果一致说明算法和参数都没取错。这个步骤很重要很多新手在抄代码时漏了一个字符、一个拼接顺序导致验证不通过又不知道是哪里出了问题。5. 实战还原一个动态签名的生成过程5.1 场景描述为了完整演示我构造一个简化但完整的场景。假设一个接口POST https://api.example.com/v1/list请求体{ page: 1, limit: 20, ts: 1700000000000, sign: 8f14e45fceea167a5a36dedd4bea2543 }其中ts是毫秒时间戳sign是签名值。我们的目标是搞清楚sign是怎么生成的。5.2 在浏览器中观察按照第四章的方法在 XHR/fetch 断点中输入list刷新页面后断点停在发送请求的位置。Call Stack 回溯后发现请求参数来自一个buildParams函数function buildParams(page, limit) { var ts String(Date.now()); var params { page: page, limit: limit, ts: ts }; params.sign getSign(params); return params; }接着进入getSign发现它调用了一个md5函数function getSign(params) { var keys Object.keys(params).sort(); var base ; for (var i 0; i keys.length; i) { base keys[i] params[keys[i]] ; } base base.slice(0, -1) keya1b2c3d4; return md5(base); }这个算法是对参数名做字典序排序。按keyvalue的形式用连接。在拼接结果末尾追加keya1b2c3d4。对整体做 MD5。虽然这是教学示例但它代表了实际逆向中最常见的签名模型。真实站点可能还会加时间戳、随机数、固定前缀等模型是类似的。5.3 用 Python 复现签名现在我们把这个算法用 Python 复现import time import hashlib def generate_sign(params: dict) - str: 根据参数生成签名 示例算法 1. 参数名按 ASCII 排序 2. 拼接 keyvalue用 连接 3. 末尾追加固定密钥 4. 整体 MD5 keys sorted(params.keys()) base .join(f{k}{params[k]} for k in keys) base base keya1b2c3d4 sign hashlib.md5(base.encode()).hexdigest() return sign if __name__ __main__: params { page: 1, limit: 20, ts: str(int(time.time() * 1000)), } params[sign] generate_sign(params) print(params) print(sign:, params[sign])运行后params[sign]应该与浏览器抓包时的签名格式一致。需要注意ts每次请求都会变化所以打印结果不会相同但算法是确定的。5.4 用 Node.js 运行原 JS 文件有时候签名算法比较复杂用 Python 重写容易出错更稳妥的方式是直接把浏览器里的 JS 函数抠出来放进 Node.js 里执行。新建一个sign.js文件内容如下// 文件路径sign.js const crypto require(crypto); function md5(str) { return crypto.createHash(md5).update(str, utf8).digest(hex); } function getSign(params) { const keys Object.keys(params).sort(); let base ; for (let i 0; i keys.length; i) { base keys[i] params[keys[i]] ; } base base.slice(0, -1) keya1b2c3d4; return md5(base); } function buildParams(page, limit) { const ts String(Date.now()); const params { page: page, limit: limit, ts: ts, }; params.sign getSign(params); return params; } module.exports { buildParams, getSign };然后在同一目录下创建run.js// 文件路径run.js const { buildParams } require(./sign); const params buildParams(1, 20); console.log(params);运行命令node run.js输出示例{ page: 1, limit: 20, ts: 1700000000000, sign: 8f14e45fceea167a5a36dedd4bea2543 }这个流程的关键点是在浏览器里调试确认算法在 Node.js 里复用原 JS 代码在 Python 里完成整体爬虫逻辑。三者不是互斥关系实际项目中往往配合使用。5.5 完整请求流程示例最后把签名生成和请求发送结合起来import time import hashlib import requests def generate_sign(params: dict) - str: keys sorted(params.keys()) base .join(f{k}{params[k]} for k in keys) base base keya1b2c3d4 return hashlib.md5(base.encode()).hexdigest() def fetch_list(page1, limit20): url https://api.example.com/v1/list params { page: page, limit: limit, ts: str(int(time.time() * 1000)), } params[sign] generate_sign(params) resp requests.post(url, jsonparams, timeout10) resp.raise_for_status() return resp.json() if __name__ __main__: data fetch_list(page1, limit20) print(data)再次提醒示例中的api.example.com、密钥a1b2c3d4均为虚构实际逆向时你需要从调试环境中获取真实算法和密钥。6. 常见问题与排查思路6.1 找到了参数但找不到生成函数有些参数名很通用比如sign全局搜索会搜出几百个结果很难直接定位。这时不要大海捞针回到 XHR 断点思路。从发送请求的位置开始回溯往往比从参数名搜索更高效。断点会直接把你带到生成参数的调用现场。6.2 函数名混乱、代码被混淆混淆代码最常见的手段是变量名压缩、字符串编码、控制流平坦化、死代码注入等。面对混淆代码建议先用格式化工具展开再从左到右逐段阅读优先关注Object.keys、Array.sort、join、charCodeAt、substring这类函数因为签名算法通常会用到它们。过度混淆的情况下也可以在关键位置打断点利用 Console 查看中间变量不断缩小范围。6.3 补环境时报错xxx is not defined当你把浏览器里扣出来的 JS 放到 Node.js 里运行时可能会遇到window is not defined、document is not defined、navigator is not defined。这是因为浏览器里的 JS 代码依赖浏览器环境对象。解决思路是“补环境”也就是在 Node.js 的沙箱中手动构造这些对象const vm require(vm); const sandbox { window: {}, document: {}, navigator: { userAgent: Mozilla/5.0 ..., }, console: console, setTimeout: setTimeout, }; vm.createContext(sandbox); const code function getEnv() { return window.location.href; } ; vm.runInContext(code, sandbox);没有实际用到的浏览器属性可以先给空对象遇到报错再逐步补充。补环境是一个反复试错的过程不要指望一次成功。6.4 浏览器能跑Node 跑不了这类问题通常是运行环境差异导致的。浏览器有window、documentNode.js 没有浏览器支持某些 APINode.js 版本不支持代码里使用了window.btoa、atob等函数而 Node.js 里没有。解决方式有两种补齐环境对象。用jsdom模拟浏览器环境虽然体积较大但兼容性更好。另外Node.js 的版本也会影响结果建议使用 LTS 版本。7. 最佳实践与工程建议7.1 先确认授权边界爬虫逆向本身是中性的技术但使用场景不同边界也不同。做技术学习、接口调试、数据采集前要确认是否符合目标网站的robots.txt和服务条款是否涉及用户隐私数据是否会给目标服务器造成压力。在测试环境中进行调试控制请求频率是工程化爬虫的基本要求。7.2 签名算法优先本地化能直接用 Python 复现的签名就不要引入 Node.js 进程因为多一个运行时就多一层维护成本和性能损耗。但也不要强行用 Python 重写复杂算法。如果签名函数逻辑复杂、依赖很多浏览器全局变量直接把 JS 抠出来用 Node.js 执行稳定性反而更高。7.3 代码分层签名逻辑独立爬虫工程建议至少拆成三层层职责示例抓取层发送请求、处理响应、失败重试fetcher.py签名层生成动态参数、维护密钥池sign.py或sign.js解析层解析响应数据、入库parser.py签名层不要和抓取逻辑混在一起否则一旦签名算法更新改起来很痛苦。7.4 日志、异常与频率控制逆向依赖的算法可能会被服务端更新所以要记录完整请求日志。日志至少包含请求 URL、请求参数、响应状态码、响应内容前 500 字符、时间戳。同时给请求增加超时、重试、随机延时和代理轮换策略避免高频请求导致 IP 被封。import time def safe_request(func, retries3): for i in range(retries): try: return func() except requests.RequestException as e: print(f请求失败: {e}, 重试 {i 1}/{retries}) time.sleep(2 ** i) raise RuntimeError(重试次数已用完)这里的2 ** i是指数退避策略第一次等待 2 秒第二次 4 秒第三次 8 秒。配合随机延时使用能显著降低触发风控的概率。7.5 监控签名有效性签名算法一旦失效请求通常会返回 401、403 或特殊错误码。建议在爬虫主流程中单独捕捉这一类响应并发出告警而不是把它当普通错误处理。最有效的监控方式是在主循环之前先请求一个最小接口来验证签名。如果最小接口返回异常立即停止当前任务等待签名更新。8. 学习路线与最后建议到这里你应该已经理解了动态参数是什么、如何分类、如何定位、如何复现也知道遇到混淆代码和补环境问题时该怎么处理。如果继续深入建议按这个顺序学习JS 语法基础函数、闭包、作用域、原型链这些是读懂前端代码的基础。常用加解密算法MD5、SHA、AES、RSA了解它们的特征和适用场景。webpack 模块化代码学习如何从打包产物中定位模块如何调用模块内部函数。AST 逆向用 AST 处理混淆代码例如把十六进制字符串还原成明文。补环境框架了解 Proxy 拦截、jsdom 模拟浏览器等方案处理更强的环境检测。验证码处理与风控对抗这属于另一个层级的工程问题需要结合具体业务场景。动手练习时优先选择公开的爬虫练习靶场和开源项目不要以真实生产环境为目标练习。练习过程中记录自己的调试思路慢慢建立一套属于自己的定位方法。爬虫逆向的核心能力不是背工具而是形成一套“从请求出发倒推前端逻辑”的思维模式。希望这篇入门内容能成为你建立这套思维模式的第一步。