爬虫JS逆向:从动态参数到签名复现的完整调试链路

📅 2026/8/27 9:40:19
爬虫JS逆向:从动态参数到签名复现的完整调试链路
爬虫和 JS 逆向这两个词放到一起时最容易劝退新手的不是加密算法而是“参数对不上”的问题。你抓包抓到的 URL 看起来完整复制到 requests 里重放却得到参数错误、签名失效或者直接返回一个奇怪的页面。原因通常只有一个请求里包含动态参数而你在重放时没有把这个参数当作“由页面 JS 实时生成的结果”来处理。这篇文章就从动态参数入手把 JS 逆向入门阶段最该看懂的排查链路讲清楚。这个主题适合刚接触爬虫、想做接口调试、或者在写前端时需要理解签名逻辑的读者。你把目标定位成“看懂参数怎么生成”就够了不需要一上来就把整套 JS 混淆方案都解开。真正值得学的不是某一条 URL 规则而是从 Network 面板到 Sources 面板、再到断点和作用域的定位方法。下面按实际落地顺序拆一遍。1. 动态参数到底是什么先区分几种容易混淆的情况1.1 参数为什么是动态的一个接口请求除了固定的路径之外通常还带 querystring、headers、body 或者 cookie。其中有一部分字段是写死的比如app_id1001、version1.0另一部分每次请求都会变化比如timestamp1710000000、sign3f9a...、trace_id...。后一类就是动态参数。服务端为什么要求参数动态常见原因有三个防止请求被原样重放。如果一条请求的签名只跟固定内容有关别人复制 URL 之后就可以无限次调用。防止参数被篡改。请求里的关键字段参与签名后改一个数字签名就不匹配。做会话和链路追踪。动态参数不一定用于安全也可能是为了标识一次请求、一个用户或者一台设备。理解了这一点你就不会看到动态参数就觉得是“加密”。它可能只是时间戳可能是随机字符串也可能是加密结果。不同情况处理方式完全不同。1.2 常见的四类动态参数以我自己的排查习惯会把动态参数分成四类时间型字段名是t、ts、timestamp、time值通常是 10 位或 13 位数字。随机型字段名是nonce、rand、uuid值是一段看起来没有规律的字符串。签名型字段名是sign、sig、token、token值通常是固定长度的十六进制或 Base64 字符串。设备/会话型字段经常出现在 cookie 或 header 里比如某些视频站点的buvid类参数用于标识浏览器环境或会话。初学者容易犯的一个误区是看到一个长字符串就默认是 AES 或 RSA 加密。实际很多长字符串只是Base64(时间戳 固定 secret)甚至只是MD5(固定字符串 当前时间)解密难度并不高。1.3 入门目标不是“破掉全部 JS”而是定位参数生成链路很多人在入门阶段就栽在“我想把整段 JS 都读懂”这个想法上。真实情况是一个业务站点可能加载了几十份 JS 文件压缩之后动辄几百 KB里面还混着 webpack 打包逻辑、自定义框架和第三方 SDK。正确思路是先用排除法把范围缩小。你要关心的不是整个 JS 系统而是“目标接口请求发出之前动态参数是在哪一行代码里被赋值、拼接、加密、写入请求的”。只要定位到一个函数再向上找调用来源链路基本就浮现了。后面所有调试技巧都是为了缩短这个定位过程。2. 从一个可复现的请求开始工具准备和对比方法2.1 推荐的最小工具组合做 JS 逆向入门不需要一开始就上复杂工具链。最小组合是浏览器推荐 Chrome 或 Edge自带开发者工具。一个 HTTP 抓包调试工具比如 Charles 或 Fiddler用于观察请求细节。Python 环境配合 requests 或 httpx用来做重放和验证。本地一个可返回请求信息的服务比如httpbin或者你自己写一个简单的 Flask/Node 服务。这里的重点是“可控”。分析自己写的接口或者公开 Demo 接口你能随时改代码、加日志、对比参数学习效率最高。不建议直接拿别人线上的高保护接口开刀因为失败成本高而且边界不好控制。2.2 先请求一次记录完整请求包动态参数分析的第一步不是打开 JS 文件狂读而是先把一次完整请求记录下来。记录内容包括请求 URLQuery 参数请求方法请求头Request BodyCookie请求发起的时间在浏览器开发者工具的 Network 面板里找到目标请求右键另存为 HAR 文件或者直接复制为 cURL 格式。这样能保证你拿到的是一份完整的、格式统一的请求快照。记录时容易忽略一个细节请求头里的大小写、顺序以及 Cookie 的具体值。很多动态参数并不是明文出现在 URL 里而是被写进了 Cookie 或者自定义 Header例如X-Token、X-Sign。2.3 用 Python 重放观察动态参数对结果的影响拿到请求快照后先用 Python 按原样重放一次。这一步的目的不是直接跑通而是确认“原样重放是否能成功”。如果原样重放成功说明动态参数要么在当前会话内有效要么服务端没有做强校验。这时你可以修改其中一个动态参数比如把时间戳改小 10 秒再请求一次观察接口返回是否变化。如果原样重放失败比如返回签名错误、参数缺失说明服务端验证了请求有效性且你缺少了某个动态生成步骤。这样你就知道接下来要回到浏览器里找参数生成逻辑。下面是一个验证思路的示例针对自建接口或公开 Demo 接口使用import requests import time import hashlib # 仅用于自建接口或公开 Demo 接口 # 假设接口规则是 sign md5(ts demo_secret path) ts str(int(time.time())) secret demo_secret path /api/weather raw ts secret path sign hashlib.md5(raw.encode(utf-8)).hexdigest() resp requests.get( http://127.0.0.1:8000/api/weather, params{ts: ts, sign: sign} ) print(resp.status_code) print(resp.text[:200])注意这段代码不是“通杀模板”。真实接口的签名规则各不相同你要做的是把“输入什么、参与签名的字段有哪些、用哪种摘要算法”这几个问题搞清楚再替换成对应逻辑。2.4 判断参数是后端返回、前端生成还是本地存储重放之后需要进一步判断动态参数的来源。判断方法很直接重新加载页面看这个参数是否在首次 HTML 返回的脚本变量里。在浏览器里清除全部站点数据再看请求是否还带这个参数。在同一个页面里连续触发两次相同请求看参数值是否变化。如果参数来自后端返回通常能在某个接口的响应里直接搜索到参数名。如果参数来自前端生成一般是 JS 在请求前用时间戳、随机数、甚至是某个加密函数计算出来。如果参数来自本地存储比如 localStorage、sessionStorage、cookie则需要先确认它是什么时候被写入的。这一步能帮你快速建立“字段来源”的概念。后面所有断点操作其实都是在回答两个问题在哪里创建、在哪里被读取。3. 在浏览器中反推参数生成位置从 Network 到 Sources3.1 先利用 XHR/fetch 断点定位触发代码当你知道某个接口的 URL但不知道请求是由哪行 JS 发起的最快的方式是使用 XHR/fetch 断点。在开发者工具的 Sources 面板右侧找到 XHR/fetch breakpoints添加需要拦截的 URL 关键字比如接口路径末尾的weather。然后重新触发页面上能发起该请求的操作浏览器会在请求发出前自动停在发起请求的那一行 JS 代码上。这个断点的价值在于它直接告诉你“请求在这里被构造”。你不需要在成千上万行代码里人工搜索。停在断点后重点看调用栈右侧的调用顺序通常能看到一条链路某个点击事件或页面加载函数业务封装函数请求工具函数XMLHttpRequest 或 fetch 调用在这条链路上动态参数往往已经作为函数参数传到了请求工具函数里。接下来需要反向寻找“参数是什么时候被塞进来的”。3.2 格式化压缩 JS并搜索参数名字符串如果请求代码是压缩后的单行代码阅读前先点击代码区域左下角的格式化按钮。格式化之后代码会从一行变多行变量名可能仍然是a、b、c这类短名称但字符串常量、函数名和注释不会全部丢失。在格式化后的代码里使用 CtrlShiftF 全局搜索参数名。搜索时建议同时尝试几种形式直接搜索ts、sign、token搜索timestamp、signature搜索 URL 中出现的签名片段的前 6 位搜索号附近的字符串拼接逻辑比如sign 、sign注意一个细节搜索到的位置不一定就是生成位置。可能是读取位置、拼接位置也可能只是日志输出位置。你要结合断点调试进一步确认。3.3 断点命中后看作用域和调用栈在可能存在生成逻辑的那一行打上断点重新触发请求。当断点命中时不要急着跳走先看右侧的 Scope 面板。Scope 面板里会展示当前函数作用域内所有变量。你关注的是动态参数变量当前的值是多少这个变量来自哪个上层作用域当前函数是否接收了外部传入的参数有没有局部变量在下一次调用前发生变化如果断点停在一行赋值语句上比如sign i.sign()说明sign由i对象的某个方法生成你得进入sign方法内部继续断点。如果停在一行字符串拼接上比如url ts ts说明生成位置在更早的代码里。3.4 用控制台直接验证一个表达式在调试过程中可以直接在 Console 面板里输入表达式验证当前上下文里的变量值。比如断点停在某个函数内部你可以在控制台输入ts查看当前值。也可以输入typeof ts判断它是数字还是字符串。还可以主动调用当前函数内部的工具方法看看输出格式是否符合请求里的参数格式。这种验证方式非常快。它能避免你反复刷新页面、重新打断点。但要注意控制台执行的是浏览器当前页面上下文中的代码如果你切换了断点位置控制台上下文也会变化。3.5 反推过程中最容易卡住的两类情况一类是参数名被混淆。真实业务代码经过打包工具处理后参数名可能变成_0x1a2b3c这种形式。这时直接搜索参数名容易搜不到。解决办法是改搜 URL 里的路径关键字或者搜接口请求的完整参数格式比如ts。另一类是参数生成被藏在异步回调里。请求不是同步构造的而是先请求一个配置接口拿到密钥后再在.then()回调里生成签名并发送正式请求。遇到这种情况要在回调内部打断点而不是在请求工具函数里打断点。4. 拆解几种常见动态参数的生成逻辑4.1 时间戳类参数时间戳参数最容易识别也最容易被新手误判。它的常见写法如下ts Date.now(); // 13 位毫秒时间戳 ts Math.floor(Date.now() / 1000); // 10 位秒级时间戳判断一个参数是不是时间戳最简单的办法是看它是否随时间变化且变化范围是否在一个合理区间内。把 10 位数字放入在线时间戳转换工具如果转换成正常时间并且时间接近当前时间那基本就是时间戳。时间戳通常不会单独作为安全措施更多时候它参与签名比如拼进sign md5(ts secret)。当你重放请求时不能直接使用抓包时记录的时间戳而要用当前时间重新生成。否则服务端会认为请求过期。4.2 哈希/摘要类参数哈希类参数的特征是固定长度比如 MD5 是 32 位十六进制SHA-1 是 40 位SHA-256 是 64 位。生成逻辑通常是先拼接一组字段再做一次摘要运算。在分析这类参数时核心是找到“拼接顺序”。同样的字段顺序不同结果完全不同。比如import hashlib # 示例仅用于自建接口测试 a param_a b param_b secret secret_key # 方式一 sign1 hashlib.md5(f{a}{b}{secret}.encode()).hexdigest() # 方式二 sign2 hashlib.md5(f{b}{a}{secret}.encode()).hexdigest() # 方式三 sign3 hashlib.md5(f{secret}{a}{b}.encode()).hexdigest()sign1、sign2、sign3完全不同。这说明即使你定位到了“用了 MD5”只要拼接顺序不对签名依然验证失败。调试验证时可以自己在控制台里用不同顺序拼接再跟请求里的签名做对比。4.3 序列化与编码类参数有些动态参数不是摘要而是把对象转成 JSON再经过 Base64 编码或者对 JSON 做 URL 编码。这类参数的特征是末尾经常有中间可能出现%或。分析时可以先在控制台里执行decodeURIComponent、atob或btoa看是否能得到可读文本。如果能直接看到{name:xxx,time:...}说明这只是编码不是加密。你只要按照编码规则还原即可。4.4 加密签名类参数当参数涉及 AES、RSA 等对称或非对称加密时分析难度会明显上升。但入门阶段不需要死磕算法细节。你要做的三件事是找到加密函数入口确认密钥或公钥从哪里来写死在 JS 里还是从某个配置接口返回确认加密对象和填充模式是否固定在浏览器调试时你可以尝试在加密函数入口打断点查看函数参数。只要参数和输出格式一致即使不懂算法内部实现也能在 Python 中调用类似算法库来复现。当然这只适用于授权分析或自建接口不要把它理解为破解他人服务的方法。4.5 动态 Cookie 和前置请求动态 Cookie 是另一个高频问题。很多接口在正式返回数据之前会先执行一个前置请求通过 JS 计算出一个 cookie 值然后再带这个 cookie 请求目标接口。这类参数有两个特点直接在请求头里看不到明显的时间戳或签名只看到一个 cookie 值。如果复制首次请求的 cookie 去重放可能第一次有效第二次就失效。排查时不要只盯目标接口还要看 Network 面板里的请求顺序。目标接口之前通常有一个独立请求比如一个 JS 文件或一个空接口响应内容是一段 JS执行后生成了 cookie。你需要记录这个前置流程而不是只复制当前请求。4.6 参数生成逻辑的识别表参数特征可能类型判断方式10位或13位数字时间戳转换成时间后是否接近当前时间32/40/64位十六进制哈希摘要用不同字段拼接顺序验证Base64字符串可能带编码解码后是否可读长度固定且不可读加密找密钥来源和加密算法入口Cookie 动态变化前置脚本写入观察请求顺序和脚本执行位置这张表只是一个起点。真实业务可能把多种方式组合在一起比如先对 JSON 做摘要再加密再编码。但只要你分清“它看起来更接近哪一种”下一步调试方向就不会偏。5. 看到加密函数后怎么验证它是不是真正生效的那一个5.1 直接看输入调用时传进来哪些变量断点停在加密函数入口时第一步不是进去看算法而是看函数接收到的参数。参数本身就是信息。假设你停在function encrypt(data, key)这一行可以通过 Console 输入data观察它是什么内容。如果data是已经拼接好的字符串说明参数生成在更上层如果data是一个对象说明函数内部可能还会做序列化。确认输入后再确认输出。你可以在 Console 里手动调用这个函数看看输出结果和请求里的动态参数是否一致。一致就说明你找到了正确的函数不一致就需要继续向调用栈上方找。5.2 打断点后看输出再用“改参数重放”验证找到可疑函数后不要只看一次结果。要在函数调用前后分别打断点对比输入输出。然后将生成结果放进 Python 请求里观察接口是否返回成功。这里有一个判断技巧如果只修改时间戳接口仍然返回成功说明签名没有绑定时间窗口如果签名正确但时间戳过期说明服务端做了时间校验。这两种情况对应不同的重放方案。但在实测时我一般会先保持签名完全不变只改请求里的某个普通字段。如果接口返回签名错误说明普通字段参与了签名计算。这样就能逐步圈出哪些字段是签名因子。5.3 判断签名是否和时间窗口绑定的常见方法你可以故意把请求里的时间戳往回调 5 分钟然后保持签名不变发送请求。如果接口直接拒绝说明服务端使用了时间窗口校验。如果仍然返回成功说明时间戳只是参与签名并没有作为过期时间。如果返回成功但结果是错误数据说明服务端可能忽略了这个参数。同样你可以把签名里的时间戳改成当前时间重新生成签名再重放。如果成功说明签名生成逻辑已经复现了。5.4 参数由异步逻辑或定时器生成时怎么处理动态参数不一定在请求函数栈里同步生成。有些页面会先通过定时器定期刷新 token浏览器在某个时间点自动更新然后请求时再读取。这类情况需要额外关注定时器和异步回调。处理思路是在 Network 面板里找 token 更新前后的两个请求对比时间点。在 Sources 面板里搜索 token 被赋值的位置打断点。在 Console 里执行setTimeout或setInterval相关逻辑观察它是否触发下一次赋值。异步逻辑并不代表你要去模拟定时器。你只需要找到“最终赋值”的函数在请求构造前调用它或者直接计算它生成的值。6. 入门阶段容易踩的坑和处理顺序6.1 搜索不到参数名先看字符串拼接和编码方式搜索参数名没结果不代表参数不存在。很可能参数名在 JS 代码里是以拼接方式出现的比如si gn或者经过编码。这时候改搜索连续字符串片段或者搜索 URL 里签名参数值的前几位命中率更高。还有一种情况是参数名本身是动态的比如从配置对象里读取字段名。你搜不到固定字符串是正常的这时应该搜索请求构造处的代码而不是参数名。6.2 断点总是断不住检查文件版本和生效位置断点断不住最常见的原因是代码被修改后浏览器没有重新加载最新版本你断点打在旧文件上。解决方法是强制刷新页面或者清缓存重新加载。另一个原因是请求由 service worker 或浏览器扩展发起业务 JS 没有走正常加载路径。这种情况下先确认请求发起来源再决定在哪里打断点。6.3 动静态参数混在一起先区分再复现一次请求里经常同时包含多个参数比如固定版本号 动态时间戳 动态签名。新手容易把所有参数都当成动态参数导致复现逻辑臃肿。正确做法是先做一次原样重放确认哪些参数是必需的。然后逐个改为固定值观察哪一次修改会导致请求失败。这样能筛出真正动态且参与校验的参数。6.4 动态 Cookie 导致第一轮请求失败先看请求顺序有些请求要两三轮才能拿到完整参数。第一轮可能返回一段脚本第二轮运行脚本生成 cookie第三轮才请求正式数据。如果直接用 Python 重放第三轮请求必然失败。处理思路是记录完整的请求顺序包括每一轮的 URL、参数、响应头和 Set-Cookie再按顺序在代码里逐步执行。不要只复制最后一个请求。6.5 记录一张排查顺序清单先看 Network目标接口的完整 URL、参数、Header、Cookie。再看 Source搜索参数名搜索 URL 关键字。再打断点在请求构造处、可疑函数入口处、赋值语句处。再看 Scope确认参数来自哪个作用域是否是异步结果。再改参数重放区分时间戳、签名因子、固定值。最后记录参数来源、生成方式、拼接顺序、过期判断。这张清单的适用范围不限于某个网站。你换成任何接口只要按照这个顺序都能把“动态参数怎么生成”这个问题拆成可执行的小任务。7. 适合初学者的练习方式与合规边界7.1 练习环境用自建 Node 服务生成动态参数如果你没有现成目标建议自己搭一个简单服务专门用来练习前端签名的定位和分析。比如用 Node.js 写一个接口要求请求里必须带ts和sign其中sign由MD5(ts secret)生成。// 示例自建签名接口仅供学习调试 const http require(http); const crypto require(crypto); const server http.createServer((req, res) { const url new URL(req.url, http://127.0.0.1:8000); const ts url.searchParams.get(ts); const sign url.searchParams.get(sign); const expect crypto.createHash(md5).update(ts demo_secret).digest(hex); if (sign expect) { res.end(ok); } else { res.end(fail); } }); server.listen(8000);然后写一个前端页面在页面里用 JS 动态生成ts和sign请求这个接口。接着你用浏览器开发者工具去分析这个自建页面练习搜索参数名、打断点、看作用域。整个过程完全可控出了任何问题都能直接看后端日志。7.2 练习思路围绕“生成-传输-验证”三步动态参数看起来复杂核心逃不过三步生成前端 JS 拿时间、随机数、固定字符串、密钥等做计算。传输结果放到 query、header、body 或 cookie 里。验证服务端读取并重新计算对比是否一致。分析时你只要把关注点放在“生成”和“传输”这两步。服务端验证方式可以通过自建接口模拟不需要去逆向别人的后端逻辑。7.3 合规边界哪些场景可以写哪些不能碰写这类文章或者做练习时有一点要始终明确动态参数分析主要用于自己开发的接口调试、公开 API 联调、前端安全测试以及提升对浏览器执行机制的理解。它不是用来绕过登录、绕过付费、批量抓取未授权数据、攻击他人系统的工具。建议遵守几条边界只分析自己有权限的服务或者明确公开的接口文档。不在文章里给出针对具体商业站点的破解步骤。不把验证码绕过、滑块破解、风控绕过作为学习目标。不传播可能造成安全风险的完整绕过代码。如果某个分析场景让你感到边界模糊就停下来换成自建环境继续练习。技术能力可以慢慢积累但合规风险不值得去冒。7.4 初学者可以把精力放在这四件事上熟悉浏览器开发者工具里的 Network、Sources、Console 三个面板。练习打断点、看作用域、读调用栈不需要看懂全部 JS。熟悉常见编码和摘要算法重点不是实现而是识别特征。学会用 Python 或 Node 按“参数拼接顺序”复现签名并反复对比验证。把这些基础打牢之后再回头处理真实接口你会发现自己面对的不再是一堆压缩代码而是一个“参数从哪里来、经过哪个函数、最终放到哪里”的明确流程。到了那个阶段动态参数就不再是劝退点而是帮你理解整个请求链路的钥匙。