☰
拆解某东h5st签名:从webpack打包产物到Python复刻与Node服务化
2026/10/11 13:53:30 网站建设 项目流程

简介:这份资源面向具备一定前端与爬虫基础的开发者,聚焦某东平台webpack打包方式下的h5st参数逆向分析,提供一套可运行的完整代码示例,帮助理解移动端模板加载机制与接口加密参数的生成逻辑。压缩包内共2个文件,包含1个Python脚本与1个JavaScript文件,分别承担请求逻辑与前端加密还原的演示职责,整体约159KB,体量轻便,便于快速阅读与本地调试。目前已有855人学习下载,说明该方向在爬虫与逆向学习群体中关注度较高。读者可从中获取webpack模块拆分的分析思路、h5st相关参数的定位与还原方法,以及Python侧调用验证的参考写法,适合作为研究前端加密与数据接口机制的实践素材。需注意,相关技术应仅用于合法合规的学习与安全测试场景,尊重平台规则与版权边界。

1. 拆开某东 h5st:webpack 打包产物里的签名参数到底藏在哪

打开某东 H5 页面,抓一个商品详情或结算请求,你会看到一串叫h5st的参数,长度不固定,逗号分隔,里面混着时间戳、随机串和一段看起来像哈希的东西。它跟sign、timestamp一起出现,少一个请求就返回「参数校验失败」。很多人第一次撞上它,以为是普通 MD5,拿参数排个序拼一拼就完事,结果怎么算都对不上——因为 h5st 不是单点哈希,而是一整套由 webpack 打包进 JS 里的签名逻辑,入口函数被拆散在多个 chunk 中,还带环境检测。

这篇讲的就是怎么把 webpack 打包后的 h5st 签名逻辑还原出来,落到能跑的完整代码。适合两类人:一是做数据采集、比价、库存监控,被 h5st 卡住的工程师;二是想系统学 webpack 逆向、搞清楚 chunk 加载与模块导出机制的人。核心思路不是硬怼混淆,而是顺着 webpack 的模块系统把签名函数「钓」出来,再用 Python 复刻。下面从定位、扣代码、补环境到复现,一步步来。

2. 定位 h5st 生成入口:从请求参数反推 webpack 模块

2.1 先确认 h5st 是前端生成还是服务端下发

动手前必须分清一件事:h5st 到底是浏览器算出来的,还是接口返回的。判断方法很直接——在 Network 面板里找发起请求前的那次响应,看有没有字段直接等于 h5st。如果没有,再在 Sources 里全局搜h5st,命中的位置通常就是生成点。

我一般会先做一次「断点验证」:在疑似生成 h5st 的代码行打上 XHR/fetch 断点,或者直接在JSON.stringify参数前下断,刷新页面看调用栈。如果调用栈里出现webpack相关的模块加载函数(形如__webpack_require__),基本可以确定签名逻辑被打包进了 bundle。

这里有个反直觉的点:h5st 往往不是在一个函数里一次算完,而是先由某个模块生成原始串,再经过另一个模块做编码和拼接。所以你在调用栈里看到的第一个命中点,通常只是「组装」环节,真正的哈希计算还在更上层。

2.2 用 webpack 模块特征锁定签名函数

webpack 打包后的代码有个稳定特征:所有模块被包在一个大对象或数组里,通过__webpack_require__(moduleId)加载。模块 ID 可能是数字,也可能是路径字符串。定位签名函数的关键,就是找到那个「被 require 进来、接收参数、返回 h5st」的模块。

常见做法是在控制台劫持__webpack_require__,把加载过的模块都打印出来:

// 在页面加载早期注入,劫持 webpack 的模块加载函数 (function () { // 保存原始 require,避免破坏原有逻辑 const originalRequire = window.__webpack_require__; if (!originalRequire) { console.log('未检测到 __webpack_require__,可能不是标准 webpack 产物'); return; } // 记录每个模块被加载的次数和导出内容 window.__moduleCache = {}; window.__webpack_require__ = function (id) { const module = originalRequire(id); // 只关心导出里有函数的模块,签名逻辑通常是函数 if (module && typeof module === 'object') { window.__moduleCache[id] = module; } return module; }; })();

这段代码的逻辑是:不改动原有加载行为,只在每次模块被 require 时把导出对象存一份。参数说明——id是 webpack 内部的模块标识,可能是数字也可能是字符串;__moduleCache用来事后检索。跑完之后,在控制台遍历__moduleCache,找导出里带sign、h5st、encrypt之类命名的函数。

提示:劫持必须在页面主 bundle 执行之前完成,否则早期模块已经加载完,抓不到。用浏览器扩展的「页面加载前注入」或者代理工具改写 HTML 都行。

2.3 从调用栈回溯到参数拼装顺序

锁定候选模块后,下一步是搞清楚它接收什么参数、按什么顺序拼。最稳的办法是在候选函数入口下断点,然后触发一次真实请求,看调用栈上一层传进来的实参。

典型 h5st 的输入包括:请求 URL 路径、请求体、时间戳、随机数、以及一个从 Cookie 或 localStorage 里取的 token。拼装顺序错了,哈希必然对不上。我习惯把断点处看到的实参逐个记下来,尤其是那个「看起来像固定盐值」的字符串——它经常是硬编码在模块顶部的常量。

如果调用栈被 webpack 的异步 chunk 打散,可以打开「Async」调用栈选项,或者在__webpack_require__.e(加载 chunk 的函数)上也下断,观察签名模块是哪个 chunk 带进来的。这一步耐心点,通常半小时内能理清完整调用链。

3. 扣出签名代码:把 webpack 模块还原成可独立运行的函数

3.1 判断哪些模块必须扣、哪些可以桩掉

webpack 产物里,签名模块往往依赖一堆工具函数:编码、加密、时间格式化。全扣下来工作量大,而且容易带出无关依赖。我的原则是——只扣「参与哈希计算」的模块,其余用桩函数替代。

判断方法:在签名函数内部逐行执行,看哪些调用真正影响了返回值。比如某个md5工具模块,如果它的输出直接进了最终串,就必须扣;而某个只用来打日志的模块,直接返回空对象即可。

常见必须扣的模块类型:哈希/加密库(md5、sha256、hmac)、Base64/Hex 编码、字符串拼接工具。可以桩掉的:网络请求封装、UI 提示、埋点上报。

3.2 用 Node 补环境跑通扣下来的模块

扣下来的代码通常不能直接在 Node 里跑,因为它依赖浏览器环境:window、document、navigator、location。补环境的核心是「用到什么补什么」,不要一上来就上完整的 jsdom,那样又重又容易引入新问题。

// 最小浏览器环境桩,按签名模块实际用到的对象补 global.window = global; global.navigator = { userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15', platform: 'iPhone', }; global.location = { href: 'https://example.com/goods/detail', search: '?skuId=10086', }; global.document = { cookie: 'your_token=abc123; other=xyz', createElement: () => ({ getContext: () => null }), }; // 有些模块会读 localStorage 里的设备标识 global.localStorage = { _data: { deviceId: 'fixed-device-id-0001' }, getItem(k) { return this._data[k] || null; }, setItem(k, v) { this._data[k] = v; }, };

逻辑说明:把window指向 Node 的global,让模块里的window.xxx能取到值;navigator和location按真实请求的 UA 和 URL 填,因为部分签名会把它们纳入计算。参数上,userAgent必须和实际发请求时一致,否则服务端校验会失败——这是最常见的翻车点之一。

补完环境后,把扣下来的模块用require或直接内联进一个文件,调用签名函数,打印结果,和浏览器里抓到的 h5st 对比。一致就说明扣对了。

3.3 处理 webpack 的 chunk 异步加载

如果签名模块不在主 bundle 里,而是异步 chunk,扣代码时会发现__webpack_require__.e被调用。这时候有两种处理方式:

一是把对应 chunk 文件也下载下来,手动把它的模块注册进__webpack_require__的模块表;二是直接在浏览器里触发一次加载,从__moduleCache里把已经加载好的模块导出对象序列化出来。

我一般用第二种,因为异步 chunk 里可能还有运行时状态。具体做法是在__moduleCache里找到目标模块,用JSON.stringify配合自定义 replacer 把函数转成字符串,再在 Node 里eval还原。注意函数里的闭包变量会丢失,所以更稳的是把整个 chunk 的源码复制出来,手动去掉__webpack_require__.e的异步包装,改成同步注册。

注意:不同批次的页面,chunk 的 hash 名会变,但模块内部逻辑通常稳定。别把 chunk 文件名写死,用「按模块特征搜索」的方式定位。

4. 复刻 h5st 生成逻辑:Python 侧完整实现与参数对齐

4.1 把 JS 签名逻辑翻译成 Python

扣出 JS 逻辑后,最终要落到 Python 里做批量请求。翻译时最容易出错的是编码细节:JS 的字符串是 UTF-16,Python 是 Unicode;JS 的charCodeAt和 Python 的ord在中文上行为不同。如果签名涉及中文参数,务必先确认 JS 侧用的是哪种编码。

下面是一个典型的 h5st 生成流程的 Python 复刻骨架:

import hashlib import time import random import hmac def gen_h5st(path: str, body: str, token: str) -> str: # 1. 时间戳,JS 用毫秒,Python 也要毫秒 ts = str(int(time.time() * 1000)) # 2. 随机串,长度和对齐方式要和 JS 侧一致 rnd = ''.join(random.choices('abcdef0123456789', k=16)) # 3. 拼装原始串,顺序必须和 JS 完全一致 raw = f"{path}&{body}&{ts}&{rnd}&{token}" # 4. 哈希,注意 JS 侧是 hex 还是 base64 digest = hashlib.sha256(raw.encode('utf-8')).hexdigest() # 5. 按 JS 的格式拼接最终 h5st return f"{ts};{rnd};{digest}"

逻辑说明:ts用毫秒是因为 JS 的Date.now()返回毫秒,用秒会导致服务端判定过期。rnd的字符集和长度必须和 JS 侧完全一致,差一位哈希就全变。raw的拼接顺序是最关键参数,必须严格按断点里看到的顺序来。digest用 hex 还是 base64,取决于 JS 侧最终串的形态——如果 h5st 里出现+/=,那就是 base64。

4.2 参数对齐:时间戳、随机数、token 的坑

时间戳的坑在于时区和精度。JS 的Date.now()是 UTC 毫秒,Python 的time.time()也是 UTC 秒,乘 1000 即可,但要注意浮点误差,用int截断。

随机数的坑在于「伪随机种子」。有些实现用Math.random(),有些用自定义的线性同余,还有的从performance.now()取低位。如果发现随机串有规律,别急着用 Python 的random,先把 JS 侧的随机函数扣出来。

token 的坑最隐蔽:它可能来自 Cookie、localStorage,也可能是页面初始化时接口下发的。如果 token 取错,签名永远对不上。验证方法是——在浏览器断点里把 token 值打印出来,和 Python 侧取到的对比,必须一字不差。

4.3 用真实请求验证签名一致性

写完 Python 后,别急着批量跑。先构造一个和浏览器完全相同的请求,只改 h5st 为 Python 生成的值,发一次看返回。如果返回正常,说明签名逻辑对了;如果返回校验失败,按下面顺序排查:

先对比 Python 和浏览器生成的 h5st 在相同输入下是否一致。如果输入相同但输出不同,问题在哈希或编码;如果输入本身就不同,问题在参数取值。我习惯在两边都打印raw原始串,逐字符对比,往往能一眼看出是多了空格还是顺序反了。

提示:服务端可能对同一 h5st 做一次性校验,重复使用会失败。验证时每次都用新生成的值,别拿旧值反复试。

5. 避坑与排查:h5st 逆向里最容易翻车的 5 个点

5.1 现象:本地算出来一模一样,发请求还是校验失败

原因通常有两个:一是请求头里还有别的签名参数(比如sign、partner)没对齐,服务端是联合校验;二是 h5st 里包含了请求体的哈希,而你 Python 侧传的 body 和实际发送的 body 有细微差异(比如 JSON 空格、字段顺序)。

解决:把浏览器里完整请求的 headers 和 body 原样复制,逐字段对比。JSON 序列化用separators=(',', ':')去掉空格,字段顺序用OrderedDict固定。

5.2 现象:昨天能跑,今天全挂

原因:h5st 的算法或盐值随版本更新变了,或者页面下发的 token 格式变了。webpack 打包产物每次发版 chunk hash 都会变,但逻辑可能微调。

解决:建立「签名自检」机制——每次跑之前先用一个已知正确的样例验证签名函数,不一致就告警。同时把扣代码的步骤脚本化,发版后重新扣一遍,别指望一份代码用半年。

5.3 现象:Node 里跑得好好的,Python 翻译后结果不同

原因:JS 和 Python 在数值精度、字符串编码、哈希默认行为上有差异。比如 JS 的parseInt和 Python 的int对前导零处理不同;JS 的btoa对非 Latin1 字符会抛错,Python 的base64不会。

解决:所有涉及编码的地方显式指定utf-8;哈希前先确认 JS 侧是对字符串还是对字节数组做哈希;遇到中文先做encodeURIComponent对齐。

5.4 现象:断点打上了,但调用栈里全是匿名函数

原因:webpack 生产模式会做压缩和混淆,函数名丢失。这是正常的,不代表你找错了地方。

解决:用「调用栈 + 参数值」定位,而不是靠函数名。在疑似函数入口打印arguments,看哪个函数的入参包含 URL 和 body,那就是签名入口。Chrome 的「Blackbox」功能可以把无关的库脚本屏蔽,让调用栈更干净。

5.5 现象:补环境后模块报xxx is not a function

原因:桩函数补得不对,或者某个依赖模块没扣全。常见的是document.createElement('canvas')返回的对象缺少getContext方法。

解决:在报错处打印缺失的对象,按需补方法。别一次性补全,缺什么补什么,补完立刻跑一次,缩小问题范围。如果某个方法只用于非签名路径,直接返回undefined或空对象即可。

6. 进阶:把 h5st 签名封装成可维护的服务

6.1 用 Node 常驻进程 + HTTP 接口暴露签名能力

Python 翻译虽然直接,但每次算法更新都要重翻一遍,维护成本高。更省事的做法是——把扣出来的 JS 代码放进一个 Node 常驻进程,对外暴露一个 HTTP 接口,Python 侧只管调用。这样算法更新时只改 JS,Python 不用动。

// sign-server.js,用原生 http 起一个签名服务 const http = require('http'); // 引入扣出来的签名模块,内部已补好环境 const { genH5st } = require('./h5st-module'); const server = http.createServer((req, res) => { if (req.method !== 'POST' || req.url !== '/sign') { res.writeHead(404); return res.end('not found'); } let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { try { const { path, body: reqBody, token } = JSON.parse(body); // 调用扣出来的签名函数,返回结果 const h5st = genH5st(path, reqBody, token); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ h5st })); } catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); } }); }); server.listen(3000, () => console.log('sign server on 3000'));

逻辑说明:这个服务只做一件事——收参数、调签名、返结果。参数path是请求路径,body是请求体字符串,token是当前会话的令牌。把签名逻辑隔离在 Node 侧的好处是,扣出来的代码几乎不用改就能用,省去翻译和反复对齐的功夫。

6.2 签名服务的稳定性与更新策略

常驻服务要考虑两点:一是 token 会过期,二是算法会更新。token 过期好办,让调用方每次传最新的 token 即可。算法更新则需要一套「热替换」机制——把签名模块单独放一个文件,更新时替换文件并重启进程,或者用require缓存清除实现不重启加载。

我一般会加一个/health接口,返回当前签名模块的版本标识(比如文件哈希)。Python 侧定时探测,发现版本变了就告警,提醒重新扣代码。这套机制跑下来,能把「突然全挂」的概率降到很低。

方案维护成本更新速度适用场景
Python 直接翻译高,每次重翻慢算法稳定、请求量小
Node 常驻服务低,只改 JS快算法常变、请求量大
浏览器自动化中,依赖页面中无法扣代码时的兜底

6.3 一个我踩过的坑:别把签名服务和采集逻辑耦合

早期我图省事,把签名函数直接内联进采集脚本,结果算法一更新,几十个脚本都要改。后来拆成独立服务,采集侧只认接口,更新时只动一个地方。这个习惯帮我省了太多后悔药。

还有一点——签名服务不要记录请求内容里的敏感字段,日志只留路径和耗时。这既是工程规范,也避免给自己埋雷。

说到底,h5st 逆向的核心不是「破解」某个哈希,而是理解 webpack 的模块组织方式,顺着它的加载机制把逻辑钓出来,再用工程化的方式维护。扣代码只是第一步,能长期稳定跑下去才是本事。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询