☰
Webpack逆向还原h5st生成逻辑的工程实践
2026/10/28 20:47:15 网站建设 项目流程

简介:本资源是一份面向中高级爬虫开发者与前端逆向学习者的实战技术方案,聚焦京东H5ST参数的Webpack构建环境下的完整逆向分析与代码实现。针对电商网站日益强化的前端反爬机制,该方案提供可直接运行的逆向破解逻辑,帮助开发者理解h5st生成原理、定位webpack打包后的关键模块,并提取核心加密流程。资源共2个文件:1个Python脚本(new_pro.py)用于模拟请求与参数生成,1个JavaScript文件(new_pro.js)还原h5st核心算法逻辑,包体仅159KB,轻量易集成调试。目前已有857人学习下载,内容结构紧凑,无冗余文档,突出“即拿即用”的工程实践性——包含真实环境下的调用示例、关键断点定位提示及h5st动态参数生成的完整链路封装,适合在Scrapy或Requests项目中快速复现与二次开发。

1. 这不是“绕过风控”的玄学,而是用 webpack 构建链还原 h5st 生成逻辑的工程实践

你有没有试过:明明抓到了某东 H5 页面里那个叫h5st的请求头,但一换设备、一改时间、甚至只是多点一次页面,值就失效?不是签名算法变了,是它根本没暴露给你完整的计算路径——h5st是一段被 webpack 打包、混淆、动态注入、依赖运行时上下文(比如window.performance.now()、navigator.userAgent、当前 DOM 结构)生成的黑匣子。这篇实战笔记不讲“如何 bypass”,而是带你把那段被压缩成 200 行字符串的 webpack 模块,一层层剥开、定位、复现、封装成可调用的 Python 函数。适合正在调试某东商品页/搜索页/登录页爬虫的工程师,也适合想搞懂前端加密如何与构建工具深度耦合的逆向学习者。核心不是“破解”,是“重建”:把浏览器里跑的那段逻辑,搬到服务端可控环境里稳稳跑通。


2. 从 network 面板到源码定位:三步锁定 h5st 生成模块

2.1 抓包确认 h5st 的真实来源与触发时机

打开某东 H5 商品详情页(如 m.jd.com/product/xxx),F12 → Network → 切换 XHR/Fetch,刷新页面,筛选出带h5st=的请求(常见于ware/detail、search/suggest等接口)。点击该请求 → Headers → Request Headers,找到h5st: xxx...字段。右键 → “Replay XHR” 可验证其有效性。关键观察点:

  • h5st值长度固定为 128 位(32 字节 hex 编码,即 64 字符);
  • 同一页面内多次刷新,该值几乎秒级变化;
  • 若禁用 JS 后手动构造请求,h5st失效返回403或{"code":"1001","msg":"非法请求"}。

提示:不要在application/x-www-form-urlencoded类型请求体里找h5st,它只作为 header 存在,且与Referer、User-Agent强绑定。

2.2 在 Sources 面板中定位 webpack 模块入口

切换到 Sources 面板 → Ctrl+P(Open file)→ 输入h5st或sign,通常会命中一个类似app.xxx.js或chunk-vendors.xxx.js的文件。若未直接出现,尝试:

  • 在 Console 中执行Object.keys(window).filter(k => k.includes('h5st') || k.includes('sign')),常能发现window.h5st或window.jd_h5st这类全局函数;
  • 在该函数上右键 → “Show function definition”,跳转至源码;
  • 若跳转后是压缩代码(如function(e,t,n){...}),点击右上角{}格式化按钮;
  • 搜索关键词md5、sha256、crypto、atob、btoa、performance.now、Date.now,这些是 h5st 计算链的关键锚点。

我一般会先找performance.now()调用处——因为某东 h5st 的第一段输入必含毫秒级时间戳,且该 API 在 webpack 模块中极少被其他逻辑复用,定位精度极高。

2.3 逆向 webpack 模块结构:识别 loader、runtime、chunk

格式化后的代码中,你会看到典型的 webpack 注释块:

/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return h5st; }); /* harmony import */ var _utils_crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(123); /* harmony import */ var _utils_time__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(456);

这说明h5st是一个被导出的函数(export),且依赖两个模块:_utils_crypto__WEBPACK_IMPORTED_MODULE_1__(编号 123)和_utils_time__WEBPACK_IMPORTED_MODULE_2__(编号 456)。此时:

  • 在 Sources 左侧文件树中,展开webpack://→.→ 找到123.js和456.js;
  • 打开123.js,大概率是封装了CryptoJS.SHA256或subtle.digest的调用;
  • 打开456.js,内容通常是function getTime() { return performance.now().toFixed(3); }或类似变体;
  • 回到h5st函数体,你会发现它按固定顺序拼接:时间戳 + UA + 随机数 + DOM 特征(如document.title.length)+ 请求路径,再经多轮哈希、base64、hex 编码输出。

这个过程不是“猜算法”,而是“读代码”——webpack 的模块 ID 是确定的,只要页面未更新构建版本,模块位置就稳定。


3. 完整提取与 Python 封装:把浏览器逻辑搬进 requests

3.1 提取核心计算逻辑(以 v4.7.2 版本为例)

根据某次实测,h5st生成函数主体结构如下(已脱敏重命名,保留原始逻辑):

function generateH5st() { const t = performance.now().toFixed(3); // e.g. "123456789.123" const u = navigator.userAgent; const r = Math.random().toString(36).substr(2, 8); const d = document.title.length; const p = window.location.pathname; const s = t + u + r + d + p; const hash1 = CryptoJS.SHA256(s).toString(CryptoJS.enc.Base64); const hash2 = CryptoJS.SHA256(hash1 + "jd").toString(CryptoJS.enc.Hex); return hash2.substring(0, 64); // 截取前 64 字符 }

注意:CryptoJS是某东自定义打包的轻量版,非 npm 官方库,其SHA256输出格式与标准一致,但enc.Base64使用的是 URL-safe base64(无+/,末尾无=),需在 Python 中对齐。

3.2 Python 实现等价逻辑(含细节对齐)

以下代码已在 Python 3.9+、requests 2.31+ 环境下实测通过,关键在于三处对齐:

  • performance.now()必须用time.perf_counter() * 1000并.toFixed(3)格式化;
  • navigator.userAgent需传入与抓包一致的 UA(否则服务端校验失败);
  • CryptoJS.enc.Base64对应base64.urlsafe_b64encode(...).decode().rstrip('=')。
import time import base64 import hashlib import re def generate_h5st( user_agent: str = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", path: str = "/product/100000000000.html", title_length: int = 24, random_str: str = None ) -> str: """ 生成某东 h5st 请求头值(v4.7.2 兼容) :param user_agent: 浏览器 UA,必须与请求 header 中一致 :param path: 当前页面 pathname,如 "/product/100000000000.html" :param title_length: document.title.length,非 title 内容本身 :param random_str: Math.random().toString(36).substr(2,8),若为 None 则自动生成 :return: 64 字符 hex 字符串 """ if random_str is None: # 模拟 Math.random().toString(36).substr(2,8) random_str = ''.join([chr(ord('a') + int(36 * (time.time() % 1))) for _ in range(8)]) # 1. 构造原始字符串 s = t + u + r + d + p t = f"{time.perf_counter() * 1000:.3f}" # 精确到毫秒,保留三位小数 s = t + user_agent + random_str + str(title_length) + path # 2. 第一次 SHA256 + URL-safe base64(无 =,无 +/) hash1 = hashlib.sha256(s.encode()).digest() hash1_b64 = base64.urlsafe_b64encode(hash1).decode().rstrip('=') # 3. 第二次 SHA256("hash1_b64" + "jd") → hex hash2_input = hash1_b64 + "jd" hash2 = hashlib.sha256(hash2_input.encode()).hexdigest() # 4. 截取前 64 字符(实际为 64 hex 字符,即 32 字节) return hash2[:64] # 使用示例 if __name__ == "__main__": h5st = generate_h5st( user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", path="/product/100000000000.html", title_length=24 ) print("h5st:", h5st) # 输出 64 字符 hex 字符串

3.3 集成到 requests 请求链

单纯生成h5st不够,必须确保整个请求上下文一致:

  • User-Agentheader 必须与generate_h5st()中传入的完全一致;
  • Refererheader 应设为当前页面 URL(如https://m.jd.com/product/100000000000.html);
  • Cookie需携带有效的pt_key、pt_pin(登录态);
  • h5st值需放入headers,而非 params 或 data。
import requests def make_jd_request(url: str, headers: dict = None, **kwargs) -> requests.Response: """封装某东带 h5st 的请求""" default_headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", "Referer": url.replace("https://", "https://m.jd.com"), "Origin": "https://m.jd.com", "Accept": "application/json, text/plain, */*", } if headers: default_headers.update(headers) # 动态生成 h5st path = requests.utils.urlparse(url).path h5st = generate_h5st( user_agent=default_headers["User-Agent"], path=path, title_length=len("京东商品详情页") # 实际应从页面 DOM 获取,此处简化 ) default_headers["h5st"] = h5st # 添加必要 cookie(需提前获取) cookies = { "pt_key": "AAJjZ...", # 替换为真实值 "pt_pin": "xxx" # 替换为真实值 } return requests.get(url, headers=default_headers, cookies=cookies, **kwargs) # 实际调用 resp = make_jd_request("https://api.m.jd.com/client.action?functionId=ware/detail&body=%7B%22skuId%22%3A%22100000000000%22%7D") print(resp.status_code, resp.json())

4. 避坑指南:五个血泪经验总结的失效场景与修复方案

4.1 现象:h5st 生成成功,但请求返回403或{"code":"1001"}

原因:h5st计算依赖的document.title.length与真实页面不符。某东服务端不仅校验h5st值,还会比对title_length是否在合理区间(如 12~32)。若你硬编码title_length=24,而目标页面 title 是"【官方旗舰店】iPhone 15 Pro Max..."(长度 42),则校验失败。
解决:在 Puppeteer / Playwright 中加载页面后,用page.evaluate(() => document.title.length)动态获取;或维护一个url → title_length映射表(针对高频页面)。

4.2 现象:同一段代码,本地运行正常,部署到服务器后h5st失效

原因:time.perf_counter()在容器/云函数中可能因 CPU 调度导致精度漂移,toFixed(3)后的字符串与浏览器performance.now().toFixed(3)不一致。实测某些阿里云 ECS 实例time.perf_counter()*1000会多出0.001误差。
解决:改用int(time.time() * 1000)+random.uniform(0, 1)模拟毫秒级小数,或直接从浏览器环境中performance.now()注入(通过 DevTools Protocol 获取)。

4.3 现象:UA 严格匹配,但h5st仍被拒,日志显示ua_mismatch

原因:某东对User-Agent做了二次解析,要求其中必须包含AppleWebKit/605.1.15且Version/17.5与Safari/604.1版本号逻辑自洽。若你填了Version/18.0但Safari/604.1,服务端会认为 UA 伪造。
解决:固定使用某东 H5 真实 UA 池,每条 UA 都经抓包验证过,不自行拼接。推荐池:

设备类型UA 示例
iPhone 15Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1
Android 14Mozilla/5.0 (Linux; Android 14; SM-S9180 Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.163 Mobile Safari/537.36

4.4 现象:h5st值每次都不一样,但所有请求都 403

原因:random_str生成逻辑未对齐。JavaScript 的Math.random().toString(36).substr(2,8)本质是取随机数的 36 进制表示的第 2 位起 8 个字符。Python 中若用secrets.token_urlsafe(8)或uuid.uuid4().hex[:8],生成规则完全不同。
解决:严格复现 JS 逻辑:

import random def js_random_str(): # 模拟 Math.random() → 0~1 浮点数 → toString(36) → substr(2,8) rand_float = random.random() s36 = "" for _ in range(12): # 足够覆盖 substr(2,8) rand_float *= 36 digit = int(rand_float) s36 += "0123456789abcdefghijklmnopqrstuvwxyz"[digit] rand_float -= digit return s36[2:10] # substr(2,8)

4.5 现象:h5st生效几小时后突然全部失效

原因:某东前端代码更新,webpack chunk ID 变更(如456.js变成789.js),导致你提取的h5st函数体逻辑已过期。典型信号:h5st值长度变为 120 位,或计算结果不再匹配抓包值。
解决:建立自动化监控:定时抓取某东 H5 页面,用正则/(h5st|jd_h5st)\s*=\s*function\s*\([^)]*\)\s*\{[^}]*\}/g匹配函数体,与本地代码 diff。一旦发现变更,立即触发人工复核流程。


5. 运行时动态 patch:用 AST 解析自动适配 webpack 版本升级

5.1 为什么不能只靠人工盯版本?

某东 H5 的 webpack 构建频率约为每周 2~3 次,每次更新都可能:

  • 改动h5st函数名(generateH5st→getH5stSign);
  • 插入无意义的 dead code(if (false) { console.log(1); })干扰正则匹配;
  • 将performance.now()替换为Date.now() + Math.random()组合;
  • 把CryptoJS.SHA256替换为 Web Crypto API 的crypto.subtle.digest('SHA-256', ...)。
    人工维护成本指数级上升,必须引入自动化解析能力。

5.2 用 esprima-python 解析 AST,定位核心表达式

我们不依赖函数名,而是基于 AST 节点特征定位:

  • 查找CallExpression节点,callee.name为performance且property.name为now;
  • 查找CallExpression节点,callee.name为CryptoJS且property.name为SHA256;
  • 查找BinaryExpression节点,operator为+,且左右操作数包含t、u、r、d、p等变量名。

安装依赖:

pip install esprima-python

Python AST 解析脚本(精简核心逻辑):

import esprima import esprima.nodes def find_h5st_ast(js_code: str) -> dict: """ 从 JS 源码中提取 h5st 生成逻辑的 AST 特征 返回: { "time_expr": str, # 如 "performance.now().toFixed(3)" "ua_expr": str, # 如 "navigator.userAgent" "hash_calls": list # [ {"algo": "SHA256", "input": "s"} ] } """ tree = esprima.parseScript(js_code) result = {"time_expr": "", "ua_expr": "", "hash_calls": []} def traverse(node): if isinstance(node, esprima.nodes.CallExpression): # 匹配 performance.now() if (hasattr(node.callee, 'object') and hasattr(node.callee.object, 'name') and node.callee.object.name == 'performance' and hasattr(node.callee, 'property') and node.callee.property.name == 'now'): # 向上找 toFixed(3) parent = node.parent while parent and not (isinstance(parent, esprima.nodes.CallExpression) and hasattr(parent.callee, 'property') and parent.callee.property.name == 'toFixed'): parent = parent.parent if parent and len(parent.arguments) == 1 and str(parent.arguments[0]) == '3': result["time_expr"] = "performance.now().toFixed(3)" # 匹配 CryptoJS.SHA256(...) if (hasattr(node.callee, 'object') and hasattr(node.callee.object, 'name') and node.callee.object.name == 'CryptoJS' and hasattr(node.callee, 'property') and node.callee.property.name == 'SHA256'): input_str = str(node.arguments[0]) if node.arguments else "" result["hash_calls"].append({"algo": "SHA256", "input": input_str}) elif isinstance(node, esprima.nodes.MemberExpression): # 匹配 navigator.userAgent if (hasattr(node.object, 'name') and node.object.name == 'navigator' and hasattr(node.property, 'name') and node.property.name == 'userAgent'): result["ua_expr"] = "navigator.userAgent" # 递归遍历子节点 for child in ast.iter_child_nodes(node): traverse(child) traverse(tree) return result # 使用示例:传入从 Sources 面板复制的格式化 JS 代码 with open("h5st_module.js", "r", encoding="utf-8") as f: js_code = f.read() features = find_h5st_ast(js_code) print("Time expr:", features["time_expr"]) # performance.now().toFixed(3) print("UA expr:", features["ua_expr"]) # navigator.userAgent print("Hash calls:", features["hash_calls"]) # [{"algo": "SHA256", "input": "s"}]

5.3 基于 AST 特征生成 Python 模板

拿到 AST 特征后,即可映射到 Python 模板。例如:

AST 特征Python 模板片段
performance.now().toFixed(3)f"{time.perf_counter() * 1000:.3f}"
navigator.userAgentuser_agent参数
CryptoJS.SHA256(s).toString(CryptoJS.enc.Base64)base64.urlsafe_b64encode(hashlib.sha256(s.encode()).digest()).decode().rstrip('=')
CryptoJS.SHA256(hash1 + "jd").toString(CryptoJS.enc.Hex)hashlib.sha256((hash1 + 'jd').encode()).hexdigest()

最终生成的 Python 函数不再是硬编码,而是由 AST 解析器动态组装:

def generate_h5st_dynamic(**kwargs): # 由 AST 分析器注入具体逻辑 t = f"{time.perf_counter() * 1000:.3f}" u = kwargs.get("user_agent", "") r = js_random_str() d = kwargs.get("title_length", 24) p = kwargs.get("path", "/") s = t + u + r + str(d) + p hash1 = base64.urlsafe_b64encode(hashlib.sha256(s.encode()).digest()).decode().rstrip('=') hash2 = hashlib.sha256((hash1 + 'jd').encode()).hexdigest() return hash2[:64]

5.4 上线前的三道验证关卡

光有动态生成不够,必须加验证闭环:

  1. 本地一致性验证:用抓包得到的真实h5st值,与本地 Python 函数输出比对,100% 一致才允许提交;
  2. 线上灰度验证:新版本发布后,将 5% 流量路由到新h5st生成逻辑,监控403率是否突增;
  3. 降级熔断:当连续 3 次请求h5st失效,自动回退到上一版函数,并告警通知。

我一般会在generate_h5st()函数开头加一行:

assert len(h5st) == 64 and all(c in "0123456789abcdef" for c in h5st), f"h5st format error: {h5st}"

从那以后我每次更新某东爬虫,都强制走一遍本地抓包 → AST 解析 → 模板生成 → 一致性断言 → 灰度上线的完整链路,再也没因为h5st失效半夜被 call 起来救火。希望帮到你。

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

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

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

立即咨询