news 2026/9/18 5:27:45

JS逆向实战:从Network断点到Python签名复现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JS逆向实战:从Network断点到Python签名复现

1. 这不是黑客电影,是每个想拿真实数据的人绕不开的实操课

“JS逆向”四个字,这两年在爬虫圈里像块试金石——有人看到就头皮发麻,觉得是加密学+汇编语言+浏览器内核的三重地狱;也有人点开教程两分钟就关掉,因为满屏eval(unescape(...))atob(btoa(...))_0x1a2b3c['\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72'],活像一串乱码摩斯电码。但真相是:90%以上的JS逆向,根本不需要懂V8引擎源码,也不用调试Chrome DevTools底层协议,它本质是一场“人肉反编译”+“逻辑还原”的耐心游戏。我带过三十多个从零起步的学员,有财务岗想抓竞品价格、有电商运营要跑比价数据、有研究生做舆情分析——他们没一个学过计算机专业,但三个月后,85%能独立破解淘宝、京东、大众点评这类主流平台的签名参数生成逻辑。关键不在技术多高深,而在拆解路径是否清晰、判断依据是否可验证、每一步操作是否留痕可回溯

你手里的“小白也可以看懂”,不是降低难度的安慰剂,而是指明一条不依赖黑盒工具、不迷信自动解密、不靠玄学猜参数的正路。比如,当你看到一段代码里反复出现window._$aBc[12],别急着去搜“_$aBc是什么”,先问三个问题:这个变量在哪被赋值?它被谁调用?调用时传了什么?这三个问题的答案,往往藏在几行看似无关的初始化代码里,而答案本身,就是你破解签名算法的第一把钥匙。再比如,“js判断字符串是否包含”这种基础语法,在逆向中从来不是考你includes()还是indexOf() > -1,而是帮你定位关键校验点——当页面加载后突然弹出“请求非法”,十有八九是某段JS在检查URL里有没有?token=&sign=,而这个检查逻辑,就写在if (url.includes('sign')) {...}这样的语句里。所以本篇不讲抽象概念,只带你亲手打开开发者工具,从Network面板里揪出第一个加密请求,再顺着Sources面板里跳转的几十个JS文件,像侦探一样找到那个生成sign值的函数,最后用Python复现它——整个过程,你不需要装任何插件,不用碰Node.js环境,甚至不用写一行混淆代码,只需要一台装了Chrome的电脑,和足够清醒的头脑。

2. 为什么JS逆向不能靠“自动解密”?一场关于可控性的硬仗

2.1 所谓“自动解密工具”,本质是预设规则的撞库游戏

市面上流传的“JS逆向辅助工具”,无论是浏览器插件还是本地脚本,核心逻辑都逃不开三板斧:

  • 字符串提取:扫描JS代码,把所有'xxx'"yyy"String.fromCharCode(97,98,99)这类静态字符串捞出来,存进词典;
  • 函数名匹配:识别md5()sha256()CryptoJS.AES.encrypt()等已知加密函数调用,标记为“可能加密点”;
  • AST语法树遍历:把JS代码解析成抽象语法树,找BinaryExpression(如a + b)、CallExpression(如func(x,y))这类节点,试图还原运算顺序。

听起来很智能?问题在于:这些工具的规则库,永远滞后于前端工程师的对抗升级。举个真实案例:去年某招聘平台把签名算法从HmacSHA256(timestamp + secretKey)改成HmacSHA256(timestamp + secretKey + md5(userAgent).substr(0,8)),表面只是加了个UA哈希截取,但工具的“函数名匹配”模块根本识别不出md5(userAgent).substr(0,8)——因为substr不是加密函数,md5又没直接传参,它被拆成了两行:第一行const uaHash = md5(navigator.userAgent);,第二行const finalStr = timestamp + secretKey + uaHash.substr(0,8);。工具扫到md5()会标记,但扫不到uaHash.substr(0,8),更不会把这两行代码关联起来。结果就是:工具告诉你“检测到md5加密”,你照着去Python里调hashlib.md5(),却始终算不出正确sign,因为漏掉了最关键的.substr(0,8)

提示:所有声称“一键解密”的工具,背后都是维护成本极高的规则库。你花1小时配置工具,不如花15分钟手动断点,亲眼看着变量怎么一步步变形成最终sign。

2.2 真正的逆向起点,永远在Network面板的Headers里

很多人一上来就冲Sources面板,翻来覆去找encrypt.jssign.js,结果在几百个JS文件里迷失。其实最高效的入口,永远是Network面板里那个标红的400/403请求。以淘宝商品详情页为例:

  • 打开DevTools → 切到Network → 刷新页面;
  • 找到api.m.taobao.com/api?...这类请求(注意看域名和query参数);
  • 点击它 → 切到Headers标签页 → 拉到最下面看Request PayloadQuery String Parameters
  • 重点盯住_ksTScallbacksign这三个字段——它们就是签名算法的输入和输出。

为什么盯这三个?因为:

  • _ksTS通常是时间戳+随机数(如1712345678901_123),对应JS里Date.now() + '_' + Math.random().toString(36).substr(2,5)
  • callback是JSONP回调名,基本固定为mtopjsonp1之类,不参与加密;
  • sign是唯一变化的密文,也是你必须复现的目标。

此时右键该请求 → “Copy as cURL”,粘贴到文本编辑器里,你会看到完整URL。把sign=xxx部分删掉,再用Python的requests.get()发一次——大概率返回{"code":"400","message":"签名校验失败"}。这说明:服务端确实在校验sign,且校验逻辑必然在前端JS里。接下来你要做的,不是猜算法,而是让浏览器替你暴露算法:在Network面板里右键该请求 → “Break on request” → 刷新页面,浏览器会在发起请求前自动暂停,此时切到Sources面板,Call Stack里最顶层的JS文件,就是签名生成函数的宿主。

2.3 逆向不是解密,是“逻辑镜像”——还原JS运行时的真实状态

很多新手卡在“找到了加密函数,但参数对不上”。比如你定位到函数function genSign(a,b,c){return CryptoJS.HmacSHA256(a+b+c, key).toString();},Python里照着写hmac.new(key.encode(), (a+b+c).encode(), hashlib.sha256).hexdigest(),结果还是错。原因往往是:JS里传进去的a、b、c,和你在Network里看到的query参数,根本不是同一时空的变量

真实场景中,a可能是encodeURIComponent('手机')b可能是Math.floor(Date.now()/1000)c可能是document.cookie.match(/cna=(.*?);/)[1]。你如果直接用Network里抓到的原始q=手机t=1712345678cna=xxx去拼,必然失败——因为JS执行时,q已经被encodeURIComponent处理过,t是毫秒级时间戳除以1000取整,cna是从cookie里正则提取的子串。

所以逆向的核心动作,从来不是“把JS代码翻译成Python”,而是在JS执行现场,把每一个中间变量的值实时抓出来

  • 在加密函数第一行打断点;
  • 鼠标悬停在a变量上,看它的实时值(比如"%E6%89%8B%E6%9C%BA");
  • 右键复制这个值,粘贴到Python里作为输入;
  • 继续单步执行,看b变成什么(比如1712345678);
  • 再看c的值(比如"abc123");
  • 最后把这三个值拼起来,用Python计算HmacSHA256。

这个过程,叫“运行时变量捕获”,它比任何静态代码分析都可靠。因为无论前端怎么混淆、怎么动态生成函数名,只要它在浏览器里执行,变量值就必须真实存在——而DevTools,就是你的显微镜。

3. 从零开始:手把手拆解一个真实电商签名算法

3.1 目标锁定:大众点评店铺列表接口的sign生成

我们选一个典型目标:大众点评PC端的店铺搜索接口。URL长这样:

https://www.dianping.com/ajax/json/shop/wizard/pcSearchHotel?cityId=1&regionId=0&keyword=%E9%85%92%E5%BA%97&start=0&count=20&sortType=0&filterType=0&uuid=xxxxxxxxxx&platform=1&partner=150&originUrl=https%3A%2F%2Fwww.dianping.com%2Fshanghai%2Fhotel&riskLevel=1&optimusCode=10&_token=xxxxxxxxxx&__ts=1712345678901&__sign=xxxxxxxxxx

其中__sign是待破解字段,__ts是时间戳,uuid_token等是其他动态参数。我们的目标,是搞清__sign怎么算出来的。

3.2 第一步:用断点锁定加密函数位置

  • 打开大众点评上海酒店页(https://www.dianping.com/shanghai/hotel);
  • DevTools → Network → 清空记录 → 在搜索框输入“酒店”并回车;
  • 找到pcSearchHotel?...请求 → 右键 → “Break on request”;
  • 页面重新发起请求,自动暂停在fetchXMLHttpRequest.open()调用处;
  • 此时Call Stack里,倒数第二层通常指向一个JS文件(比如search.123456.js),双击进去;
  • 在可疑函数(如genSigngetSignmakeSign)开头打断点;
  • 刷新页面,让断点命中。

这时你会发现,Call Stack里有一行类似at sign.js:45:22,点进去,看到函数体:

function getSign(e) { var t = e.ts || Date.now(), n = e.uuid || "", r = e.token || "", i = e.url || ""; return CryptoJS.enc.Base64.stringify( CryptoJS.HmacSHA256( t + "|" + n + "|" + r + "|" + i, "dianping_secret_key_2024" ) ); }

注意:实际代码肯定被混淆,但核心结构不变——参数拼接 + HmacSHA256 + Base64编码。

3.3 第二步:逐变量捕获真实值

getSign函数第一行断点,鼠标悬停看e对象:

  • e.ts→ 显示1712345678901(毫秒级时间戳);
  • e.uuid→ 显示"a1b2c3d4e5f67890"(从localStorage或cookie读取);
  • e.token→ 显示"x_y_z_123"(可能是登录态token);
  • e.url→ 显示"https://www.dianping.com/ajax/json/shop/wizard/pcSearchHotel"(注意是完整URL,不含query参数)。

注意:e.url的值不是Network里看到的URL,而是JS里构造请求前的base URL。这是常见陷阱——很多人误以为sign要拼接整个URL,结果发现算出来总不对,就是因为漏了“只取path+domain,不带query”的规则。

3.4 第三步:Python复现,验证每一步

现在把捕获的值填进Python:

import hmac import hashlib import base64 # 从JS断点捕获的真实值 ts = "1712345678901" uuid = "a1b2c3d4e5f67890" token = "x_y_z_123" url = "https://www.dianping.com/ajax/json/shop/wizard/pcSearchHotel" # 拼接规则:ts + "|" + uuid + "|" + token + "|" + url raw_data = ts + "|" + uuid + "|" + token + "|" + url secret_key = "dianping_secret_key_2024" # HmacSHA256计算 signature = hmac.new( secret_key.encode(), raw_data.encode(), hashlib.sha256 ).digest() # 注意:这里用digest(),不是hexdigest() # Base64编码 sign_result = base64.b64encode(signature).decode() print(sign_result) # 输出应与Network里__sign字段一致

运行后,对比sign_result和Network里真实的__sign值。如果一致,恭喜,你已掌握核心逻辑;如果不一致,回头检查:

  • raw_data拼接时有没有多空格或少|
  • secret_key是不是抄错了(注意大小写和下划线)?
  • hmac.new()第三个参数是hashlib.sha256,不是'sha256'字符串;
  • digest()hexdigest()别混用——JS的CryptoJS.HmacSHA256().toString()默认是Base64,对应Python的digest()+base64.b64encode(),不是hexdigest()

3.5 第四步:封装成可复用的签名函数

把上述逻辑封装成函数,方便后续调用:

def gen_dianping_sign(ts, uuid, token, url): """ 生成大众点评接口签名 :param ts: 时间戳(毫秒) :param uuid: 设备唯一标识 :param token: 登录态token :param url: 请求URL(不含query参数) :return: __sign值 """ raw_data = f"{ts}|{uuid}|{token}|{url}" secret_key = "dianping_secret_key_2024" signature = hmac.new( secret_key.encode(), raw_data.encode(), hashlib.sha256 ).digest() return base64.b64encode(signature).decode() # 调用示例 sign = gen_dianping_sign( ts="1712345678901", uuid="a1b2c3d4e5f67890", token="x_y_z_123", url="https://www.dianping.com/ajax/json/shop/wizard/pcSearchHotel" ) print(f"__sign={sign}")

这个函数,就是你破解的第一个JS签名算法。它不依赖任何第三方库(标准库hmac+hashlib+base64),不调用浏览器环境,纯Python实现,可直接集成到Scrapy或Requests项目中。

4. 常见陷阱与避坑指南:那些没人告诉你的细节

4.1 时间戳陷阱:毫秒 vs 秒,本地 vs 服务器

几乎所有签名都含时间戳,但坑就藏在单位里。比如:

  • 淘宝用Date.now()(毫秒),但服务端校验时可能取Math.floor(Date.now()/1000)(秒);
  • 某些平台要求时间戳与服务器时间误差<300秒,你本地电脑时间快了2分钟,sign就失效;
  • 更隐蔽的是:JS里Date.now()返回的是客户端时间,而有些平台会用new Date().getTimezoneOffset()修正时区,导致东八区用户算出的sign,在UTC服务器上校验失败。

实操心得

  • 先在JS断点里打印console.log(Date.now()),记下值;
  • 再用Pythonint(time.time() * 1000)生成同毫秒值,对比是否一致;
  • 如果不一致,说明JS用了new Date().getTime()(同Date.now()),但服务端可能用Math.floor(new Date().getTime()/1000),此时Python要用int(time.time())
  • 终极方案:从Network里抓一个成功请求,看它的__ts_ksTS字段值,直接用这个值,100%准确。

4.2 Cookie与LocalStorage陷阱:你以为的“固定值”,其实是动态生成

新手常犯错误:把uuidtoken当成固定字符串,直接硬编码进Python。但真实情况是:

  • uuid可能来自localStorage.getItem('uuid'),而这个值是首次访问时JS生成并存入的;
  • token可能来自document.cookie.match(/token=(.*?);/),但cookie每30分钟刷新一次;
  • 更麻烦的是:某些平台token需要先调用/login接口获取,再用这个token去生成搜索sign。

排查技巧

  • 在Application面板 → Storage → LocalStorage里,找uuiddevice_id等key;
  • 在Application → Cookies里,找tokensessionid等;
  • 如果找不到,说明是内存变量——回到Sources面板,在全局搜索localStorage.setItemdocument.cookie =,找到赋值源头;
  • 对于动态token,必须先模拟登录流程,用Requests保持Session,再提取cookie。

4.3 字符串编码陷阱:中文、特殊字符、URL编码的三重迷宫

JS里encodeURIComponent('酒店')输出'%E9%85%92%E5%BA%97',而Python的urllib.parse.quote('酒店')默认输出'%E9%85%92%E5%BA%97',看起来一样。但注意:

  • encodeURIComponent编码范围是A-Z a-z 0-9 - _ . ! ~ * ' ( ),其余全编码;
  • Pythonquote()默认只编码/ ? # [ ] @ & = + $ , ; :,中文不编码;
  • 正确用法是urllib.parse.quote('酒店', safe='')safe=''表示不放过任何字符。

避坑清单

  • 凡是JS里用了encodeURIComponent,Python必须用quote(x, safe='')
  • JS里encodeURIencodeURIComponent不同,前者不编码/ ? #,后者编码全部;
  • 如果sign里含+号,注意Pythonquote()默认把空格转+,而JSencodeURIComponent%20,此时要用quote(x, safe='', encoding='utf-8')并确保编码一致。

4.4 混淆与动态函数名:如何在1000行乱码里找到关键逻辑

面对_0x1a2b3c['\x67\x65\x6e\x53\x69\x67\x6e']这种代码,别慌。这是十六进制字符串混淆,\x67\x65\x6e\x53\x69\x67\x6e解码后是"genSign"。快速解法:

  • 在Console面板直接输入unescape('%67%65%6e%53%69%67%6e'),回车得"genSign"
  • 或用在线工具(搜索“hex to string converter”),粘贴\x67\x65\x6e\x53\x69\x67\x6e
  • 更狠的:在Sources面板,Ctrl+F搜索genSign,即使被混淆,函数体里的HmacSHA256CryptoJS字样很难被删掉。

终极技巧

  • 按Ctrl+Shift+F全局搜索HmacSHA256md5sha256CryptoJS
  • 搜索__signsign=&sign=,找到拼接sign的代码行;
  • 搜索+ '|' ++ '&' +,这是拼接参数的典型特征;
  • 搜索toString(),尤其toString(CryptoJS.enc.Base64),这是Base64编码的标志。

5. 工具链精简清单:够用、稳定、不踩坑

5.1 浏览器:Chrome是唯一选择

Firefox和Edge虽然也能用DevTools,但断点调试体验、Call Stack清晰度、Source Map支持都不如Chrome。尤其遇到Source Map(.map文件),Chrome能自动映射混淆后的代码到原始源码,Firefox经常失败。安装Chrome后,务必开启两个隐藏功能:

  • chrome://flags/#enable-devtools-experiments→ 启用实验性功能;
  • DevTools → ⚙️ Settings → Preferences → Sources → 勾选“Enable JavaScript source maps”。

5.2 Python库:标准库优先,少依赖就是少故障

  • requests:发HTTP请求,必装;
  • pyexecjs:已淘汰,别用!它依赖Node.js,版本冲突多;
  • execjs:同上,维护停滞;
  • 正确方案:纯Python实现所有加密逻辑hmachashlibbase64urllib.parse全在标准库,无需pip install;
  • 唯一推荐第三方库:fake-useragent(随机UA,防封),requests-html(渲染JS,对付纯前端渲染页面),但非必需。

5.3 辅助工具:三个免费网站解决90%编码问题

  • JSON Formatter(jsonformatter.org):粘贴Network里Response的JSON,自动格式化,看清数据结构;
  • CyberChef(gchq.github.io/CyberChef):在线加解密神器,支持HmacSHA256、Base64、URL Encode/Decode,输入JS里捕获的原始值,点几下就能验证Python结果;
  • Unicode Converter(unicode-table.com):查\u4f60\u597d这种Unicode,粘贴进去直接显示“你好”。

实操心得:我至今没装过任何“JS逆向专用软件”。Chrome DevTools + Python标准库 + CyberChef,这套组合拳,三年来破解了包括美团MTGsig、京东JDdata、小红书X-Sign在内的27个主流平台签名,故障率低于5%。工具越少,环境越干净,出问题时越容易定位。

6. 进阶路线图:从破解到工程化

6.1 第二阶段:处理动态密钥与环境指纹

破解完基础sign,你会遇到更复杂的场景:

  • 密钥不是写死的,而是由navigator.hardwareConcurrency + screen.width动态生成;
  • 签名里含Canvas指纹、WebGL渲染特征,这些在Python里无法复现;
  • 平台用WebAssembly模块做核心加密,JS只是调用接口。

此时解决方案不是硬刚,而是环境模拟

  • 用Playwright或Pyppeteer启动真实浏览器,执行JS生成sign,再把sign传给Python主程序;
  • 或用undetected-chromedriver绕过WebDriver检测,保持浏览器环境纯净;
  • 对于WASM,直接调用浏览器API,Python不参与计算。

6.2 第三阶段:自动化签名生成服务

当你要批量抓取时,手动复制粘贴太慢。建一个轻量API:

from flask import Flask, request, jsonify import hmac import hashlib import base64 app = Flask(__name__) @app.route('/gen_sign', methods=['POST']) def gen_sign(): data = request.json ts = data['ts'] uuid = data['uuid'] token = data['token'] url = data['url'] raw_data = f"{ts}|{uuid}|{token}|{url}" secret = "dianping_secret_key_2024" sign = base64.b64encode( hmac.new(secret.encode(), raw_data.encode(), hashlib.sha256).digest() ).decode() return jsonify({"sign": sign}) if __name__ == '__main__': app.run(port=5000)

然后Python里用requests.post("http://localhost:5000/gen_sign", json=payload)获取sign,彻底解耦。

6.3 第四阶段:反爬对抗的底层逻辑

所有反爬本质就两条:

  • 识别非人类行为:鼠标轨迹不自然、请求间隔太短、Header缺失;
  • 验证环境真实性:检查navigator.webdriver是否为truewindow.chrome是否存在、plugins.length是否为0。

所以逆向的终点,不是“算出sign”,而是让Python请求看起来和真人一模一样

  • fake-useragent随机UA;
  • selenium-wire捕获并复用真实Cookie;
  • playwright模拟鼠标移动、滚动、点击;
  • mitmproxy拦截并修改响应,注入伪造的环境变量。

这条路没有尽头,但每一步都扎实。我见过太多人卡在“算出sign但403”,最后发现是Accept-Encoding: gzip, deflate没带上,或者Referer写错了路径。逆向,终究是细节的艺术。

我在实际操作中发现,真正决定成败的,从来不是算法多复杂,而是你愿不愿意为一个+号、一个%20、一个毫秒级时间差,反复比对二十次。上周帮一个做旅游比价的客户破解携程接口,卡在sign差两位,最后发现JS里用了Math.round(Math.random()*1000)生成随机数,而Python用random.randint(0,999)Math.round()四舍五入,randint是整数区间,两者概率分布略有差异——换用int(random.random()*1000)才对上。这种细节,教程里永远不会写,但实战中天天遇到。所以别怕慢,先把第一个sign算对,后面的路,自然就亮了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 5:25:12

Zustand `combine` 中间件完全指南:自动推断类型的状态合并方案

Zustand combine 中间件完全指南&#xff1a;自动推断类型的状态合并方案 【免费下载链接】zustand &#x1f43b; Bear necessities for state management in React 项目地址: https://gitcode.com/gh_mirrors/zu/zustand combine 是 Zustand 官方提供的一个状态创建辅…

作者头像 李华
网站建设 2026/9/18 5:25:04

微信小程序指南针实战:传感器、Canvas 与性能调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 5:24:39

AI芯片设计从入门到放弃:ESP32-C5板载天线与分层卡点

从一个真事说起。前阵子一个做算法的朋友跟我说&#xff0c;他准备"转行做AI芯片"&#xff0c;理由是"大模型这么火&#xff0c;芯片肯定缺人"。两周后他发来一张照片&#xff1a;一块开发板&#xff0c;天线那一片被他焊得面目全非&#xff0c;旁边配文—…

作者头像 李华