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.js或sign.js,结果在几百个JS文件里迷失。其实最高效的入口,永远是Network面板里那个标红的400/403请求。以淘宝商品详情页为例:
- 打开DevTools → 切到Network → 刷新页面;
- 找到
api.m.taobao.com/api?...这类请求(注意看域名和query参数); - 点击它 → 切到Headers标签页 → 拉到最下面看Request Payload或Query String Parameters;
- 重点盯住
_ksTS、callback、sign这三个字段——它们就是签名算法的输入和输出。
为什么盯这三个?因为:
_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=1712345678、cna=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®ionId=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”; - 页面重新发起请求,自动暂停在
fetch或XMLHttpRequest.open()调用处; - 此时Call Stack里,倒数第二层通常指向一个JS文件(比如
search.123456.js),双击进去; - 在可疑函数(如
genSign、getSign、makeSign)开头打断点; - 刷新页面,让断点命中。
这时你会发现,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()),记下值; - 再用Python
int(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陷阱:你以为的“固定值”,其实是动态生成
新手常犯错误:把uuid或token当成固定字符串,直接硬编码进Python。但真实情况是:
uuid可能来自localStorage.getItem('uuid'),而这个值是首次访问时JS生成并存入的;token可能来自document.cookie.match(/token=(.*?);/),但cookie每30分钟刷新一次;- 更麻烦的是:某些平台
token需要先调用/login接口获取,再用这个token去生成搜索sign。
排查技巧:
- 在Application面板 → Storage → LocalStorage里,找
uuid、device_id等key; - 在Application → Cookies里,找
token、sessionid等; - 如果找不到,说明是内存变量——回到Sources面板,在全局搜索
localStorage.setItem或document.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 - _ . ! ~ * ' ( ),其余全编码;- Python
quote()默认只编码/ ? # [ ] @ & = + $ , ; :,中文不编码; - 正确用法是
urllib.parse.quote('酒店', safe=''),safe=''表示不放过任何字符。
避坑清单:
- 凡是JS里用了
encodeURIComponent,Python必须用quote(x, safe=''); - JS里
encodeURI和encodeURIComponent不同,前者不编码/ ? #,后者编码全部; - 如果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,即使被混淆,函数体里的HmacSHA256或CryptoJS字样很难被删掉。
终极技巧:
- 按Ctrl+Shift+F全局搜索
HmacSHA256、md5、sha256、CryptoJS; - 搜索
__sign、sign=、&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实现所有加密逻辑。
hmac、hashlib、base64、urllib.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是否为true、window.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算对,后面的路,自然就亮了。