ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Webpack逆向还原h5st生成逻辑的工程实践

Webpack逆向还原h5st生成逻辑的工程实践 简介本资源是一份面向中高级爬虫开发者与前端逆向学习者的实战技术方案聚焦京东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/xxxF12 → 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 面板 → CtrlPOpen 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 封装把浏览器逻辑搬进 requests3.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_agentMozilla/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_length24 ) print(h5st:, h5st) # 输出 64 字符 hex 字符串3.3 集成到 requests 请求链单纯生成h5st不够必须确保整个请求上下文一致User-Agentheader 必须与generate_h5st()中传入的完全一致Refererheader 应设为当前页面 URL如https://m.jd.com/product/100000000000.htmlCookie需携带有效的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_agentdefault_headers[User-Agent], pathpath, title_lengthlen(京东商品详情页) # 实际应从页面 DOM 获取此处简化 ) default_headers[h5st] h5st # 添加必要 cookie需提前获取 cookies { pt_key: AAJjZ..., # 替换为真实值 pt_pin: xxx # 替换为真实值 } return requests.get(url, headersdefault_headers, cookiescookies, **kwargs) # 实际调用 resp make_jd_request(https://api.m.jd.com/client.action?functionIdware/detailbody%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_length24而目标页面 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.1Android 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.364.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 codeif (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-pythonPython 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, encodingutf-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 上线前的三道验证关卡光有动态生成不够必须加验证闭环本地一致性验证用抓包得到的真实h5st值与本地 Python 函数输出比对100% 一致才允许提交线上灰度验证新版本发布后将 5% 流量路由到新h5st生成逻辑监控403率是否突增降级熔断当连续 3 次请求h5st失效自动回退到上一版函数并告警通知。我一般会在generate_h5st()函数开头加一行assert len(h5st) 64 and all(c in 0123456789abcdef for c in h5st), fh5st format error: {h5st}从那以后我每次更新某东爬虫都强制走一遍本地抓包 → AST 解析 → 模板生成 → 一致性断言 → 灰度上线的完整链路再也没因为h5st失效半夜被 call 起来救火。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表