5st生成邏輯的工程實(shí)踐)
簡(jiǎn)介本資源是一份面向中高級(jí)爬蟲(chóng)開(kāi)發(fā)者與前端逆向?qū)W習(xí)者的實(shí)戰(zhàn)技術(shù)方案聚焦京東H5ST參數(shù)的Webpack構(gòu)建環(huán)境下的完整逆向分析與代碼實(shí)現(xiàn)。針對(duì)電商網(wǎng)站日益強(qiáng)化的前端反爬機(jī)制該方案提供可直接運(yùn)行的逆向破解邏輯幫助開(kāi)發(fā)者理解h5st生成原理、定位webpack打包后的關(guān)鍵模塊并提取核心加密流程。資源共2個(gè)文件1個(gè)Python腳本new_pro.py用于模擬請(qǐng)求與參數(shù)生成1個(gè)JavaScript文件new_pro.js還原h(huán)5st核心算法邏輯包體僅159KB輕量易集成調(diào)試。目前已有857人學(xué)習(xí)下載內(nèi)容結(jié)構(gòu)緊湊無(wú)冗余文檔突出“即拿即用”的工程實(shí)踐性——包含真實(shí)環(huán)境下的調(diào)用示例、關(guān)鍵斷點(diǎn)定位提示及h5st動(dòng)態(tài)參數(shù)生成的完整鏈路封裝適合在Scrapy或Requests項(xiàng)目中快速?gòu)?fù)現(xiàn)與二次開(kāi)發(fā)。1. 這不是“繞過(guò)風(fēng)控”的玄學(xué)而是用 webpack 構(gòu)建鏈還原 h5st 生成邏輯的工程實(shí)踐你有沒(méi)有試過(guò)明明抓到了某東 H5 頁(yè)面里那個(gè)叫h5st的請(qǐng)求頭但一換設(shè)備、一改時(shí)間、甚至只是多點(diǎn)一次頁(yè)面值就失效不是簽名算法變了是它根本沒(méi)暴露給你完整的計(jì)算路徑——h5st是一段被 webpack 打包、混淆、動(dòng)態(tài)注入、依賴運(yùn)行時(shí)上下文比如window.performance.now()、navigator.userAgent、當(dāng)前 DOM 結(jié)構(gòu)生成的黑匣子。這篇實(shí)戰(zhàn)筆記不講“如何 bypass”而是帶你把那段被壓縮成 200 行字符串的 webpack 模塊一層層剝開(kāi)、定位、復(fù)現(xiàn)、封裝成可調(diào)用的 Python 函數(shù)。適合正在調(diào)試某東商品頁(yè)/搜索頁(yè)/登錄頁(yè)爬蟲(chóng)的工程師也適合想搞懂前端加密如何與構(gòu)建工具深度耦合的逆向?qū)W習(xí)者。核心不是“破解”是“重建”把瀏覽器里跑的那段邏輯搬到服務(wù)端可控環(huán)境里穩(wěn)穩(wěn)跑通。2. 從 network 面板到源碼定位三步鎖定 h5st 生成模塊2.1 抓包確認(rèn) h5st 的真實(shí)來(lái)源與觸發(fā)時(shí)機(jī)打開(kāi)某東 H5 商品詳情頁(yè)如 m.jd.com/product/xxxF12 → Network → 切換 XHR/Fetch刷新頁(yè)面篩選出帶h5st的請(qǐng)求常見(jiàn)于ware/detail、search/suggest等接口。點(diǎn)擊該請(qǐng)求 → Headers → Request Headers找到h5st: xxx...字段。右鍵 → “Replay XHR” 可驗(yàn)證其有效性。關(guān)鍵觀察點(diǎn)h5st值長(zhǎng)度固定為 128 位32 字節(jié) hex 編碼即 64 字符同一頁(yè)面內(nèi)多次刷新該值幾乎秒級(jí)變化若禁用 JS 后手動(dòng)構(gòu)造請(qǐng)求h5st失效返回403或{code:1001,msg:非法請(qǐng)求}。提示不要在application/x-www-form-urlencoded類型請(qǐng)求體里找h5st它只作為 header 存在且與Referer、User-Agent強(qiáng)綁定。2.2 在 Sources 面板中定位 webpack 模塊入口切換到 Sources 面板 → CtrlPOpen file→ 輸入h5st或sign通常會(huì)命中一個(gè)類似app.xxx.js或chunk-vendors.xxx.js的文件。若未直接出現(xiàn)嘗試在 Console 中執(zhí)行Object.keys(window).filter(k k.includes(h5st) || k.includes(sign))常能發(fā)現(xiàn)window.h5st或window.jd_h5st這類全局函數(shù)在該函數(shù)上右鍵 → “Show function definition”跳轉(zhuǎn)至源碼若跳轉(zhuǎn)后是壓縮代碼如function(e,t,n){...}點(diǎn)擊右上角{}格式化按鈕搜索關(guān)鍵詞md5、sha256、crypto、atob、btoa、performance.now、Date.now這些是 h5st 計(jì)算鏈的關(guān)鍵錨點(diǎn)。我一般會(huì)先找performance.now()調(diào)用處——因?yàn)槟硸| h5st 的第一段輸入必含毫秒級(jí)時(shí)間戳且該 API 在 webpack 模塊中極少被其他邏輯復(fù)用定位精度極高。2.3 逆向 webpack 模塊結(jié)構(gòu)識(shí)別 loader、runtime、chunk格式化后的代碼中你會(huì)看到典型的 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);這說(shuō)明h5st是一個(gè)被導(dǎo)出的函數(shù)export且依賴兩個(gè)模塊_utils_crypto__WEBPACK_IMPORTED_MODULE_1__編號(hào) 123和_utils_time__WEBPACK_IMPORTED_MODULE_2__編號(hào) 456。此時(shí)在 Sources 左側(cè)文件樹(shù)中展開(kāi)webpack://→.→ 找到123.js和456.js打開(kāi)123.js大概率是封裝了CryptoJS.SHA256或subtle.digest的調(diào)用打開(kāi)456.js內(nèi)容通常是function getTime() { return performance.now().toFixed(3); }或類似變體回到h5st函數(shù)體你會(huì)發(fā)現(xiàn)它按固定順序拼接時(shí)間戳 UA 隨機(jī)數(shù) DOM 特征如document.title.length 請(qǐng)求路徑再經(jīng)多輪哈希、base64、hex 編碼輸出。這個(gè)過(guò)程不是“猜算法”而是“讀代碼”——webpack 的模塊 ID 是確定的只要頁(yè)面未更新構(gòu)建版本模塊位置就穩(wěn)定。3. 完整提取與 Python 封裝把瀏覽器邏輯搬進(jìn) requests3.1 提取核心計(jì)算邏輯以 v4.7.2 版本為例根據(jù)某次實(shí)測(cè)h5st生成函數(shù)主體結(jié)構(gòu)如下已脫敏重命名保留原始邏輯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 官方庫(kù)其SHA256輸出格式與標(biāo)準(zhǔn)一致但enc.Base64使用的是 URL-safe base64無(wú)/末尾無(wú)需在 Python 中對(duì)齊。3.2 Python 實(shí)現(xiàn)等價(jià)邏輯含細(xì)節(jié)對(duì)齊以下代碼已在 Python 3.9、requests 2.31 環(huán)境下實(shí)測(cè)通過(guò)關(guān)鍵在于三處對(duì)齊performance.now()必須用time.perf_counter() * 1000并.toFixed(3)格式化navigator.userAgent需傳入與抓包一致的 UA否則服務(wù)端校驗(yàn)失敗CryptoJS.enc.Base64對(duì)應(yīng)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 請(qǐng)求頭值v4.7.2 兼容 :param user_agent: 瀏覽器 UA必須與請(qǐng)求 header 中一致 :param path: 當(dāng)前頁(yè)面 pathname如 /product/100000000000.html :param title_length: document.title.length非 title 內(nèi)容本身 :param random_str: Math.random().toString(36).substr(2,8)若為 None 則自動(dòng)生成 :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. 構(gòu)造原始字符串 s t u r d p t f{time.perf_counter() * 1000:.3f} # 精確到毫秒保留三位小數(shù) s t user_agent random_str str(title_length) path # 2. 第一次 SHA256 URL-safe base64無(wú) 無(wú) / 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 字符實(shí)際為 64 hex 字符即 32 字節(jié) 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 請(qǐng)求鏈單純生成h5st不夠必須確保整個(gè)請(qǐng)求上下文一致User-Agentheader 必須與generate_h5st()中傳入的完全一致Refererheader 應(yīng)設(shè)為當(dāng)前頁(yè)面 URL如https://m.jd.com/product/100000000000.htmlCookie需攜帶有效的pt_key、pt_pin登錄態(tài)h5st值需放入headers而非 params 或 data。import requests def make_jd_request(url: str, headers: dict None, **kwargs) - requests.Response: 封裝某東帶 h5st 的請(qǐng)求 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) # 動(dòng)態(tài)生成 h5st path requests.utils.urlparse(url).path h5st generate_h5st( user_agentdefault_headers[User-Agent], pathpath, title_lengthlen(京東商品詳情頁(yè)) # 實(shí)際應(yīng)從頁(yè)面 DOM 獲取此處簡(jiǎn)化 ) default_headers[h5st] h5st # 添加必要 cookie需提前獲取 cookies { pt_key: AAJjZ..., # 替換為真實(shí)值 pt_pin: xxx # 替換為真實(shí)值 } return requests.get(url, headersdefault_headers, cookiescookies, **kwargs) # 實(shí)際調(diào)用 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. 避坑指南五個(gè)血淚經(jīng)驗(yàn)總結(jié)的失效場(chǎng)景與修復(fù)方案4.1 現(xiàn)象h5st 生成成功但請(qǐng)求返回403或{code:1001}原因h5st計(jì)算依賴的document.title.length與真實(shí)頁(yè)面不符。某東服務(wù)端不僅校驗(yàn)h5st值還會(huì)比對(duì)title_length是否在合理區(qū)間如 12~32。若你硬編碼title_length24而目標(biāo)頁(yè)面 title 是【官方旗艦店】iPhone 15 Pro Max...長(zhǎng)度 42則校驗(yàn)失敗。解決在 Puppeteer / Playwright 中加載頁(yè)面后用page.evaluate(() document.title.length)動(dòng)態(tài)獲取或維護(hù)一個(gè)url → title_length映射表針對(duì)高頻頁(yè)面。4.2 現(xiàn)象同一段代碼本地運(yùn)行正常部署到服務(wù)器后h5st失效原因time.perf_counter()在容器/云函數(shù)中可能因 CPU 調(diào)度導(dǎo)致精度漂移toFixed(3)后的字符串與瀏覽器performance.now().toFixed(3)不一致。實(shí)測(cè)某些阿里云 ECS 實(shí)例time.perf_counter()*1000會(huì)多出0.001誤差。解決改用int(time.time() * 1000)random.uniform(0, 1)模擬毫秒級(jí)小數(shù)或直接從瀏覽器環(huán)境中performance.now()注入通過(guò) DevTools Protocol 獲取。4.3 現(xiàn)象UA 嚴(yán)格匹配但h5st仍被拒日志顯示ua_mismatch原因某東對(duì)User-Agent做了二次解析要求其中必須包含AppleWebKit/605.1.15且Version/17.5與Safari/604.1版本號(hào)邏輯自洽。若你填了Version/18.0但Safari/604.1服務(wù)端會(huì)認(rèn)為 UA 偽造。解決固定使用某東 H5 真實(shí) UA 池每條 UA 都經(jīng)抓包驗(yàn)證過(guò)不自行拼接。推薦池設(shè)備類型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 現(xiàn)象h5st值每次都不一樣但所有請(qǐng)求都 403原因random_str生成邏輯未對(duì)齊。JavaScript 的Math.random().toString(36).substr(2,8)本質(zhì)是取隨機(jī)數(shù)的 36 進(jìn)制表示的第 2 位起 8 個(gè)字符。Python 中若用secrets.token_urlsafe(8)或uuid.uuid4().hex[:8]生成規(guī)則完全不同。解決嚴(yán)格復(fù)現(xiàn) JS 邏輯import random def js_random_str(): # 模擬 Math.random() → 0~1 浮點(diǎn)數(shù) → 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 現(xiàn)象h5st生效幾小時(shí)后突然全部失效原因某東前端代碼更新webpack chunk ID 變更如456.js變成789.js導(dǎo)致你提取的h5st函數(shù)體邏輯已過(guò)期。典型信號(hào)h5st值長(zhǎng)度變?yōu)?120 位或計(jì)算結(jié)果不再匹配抓包值。解決建立自動(dòng)化監(jiān)控定時(shí)抓取某東 H5 頁(yè)面用正則/(h5st|jd_h5st)\s*\s*function\s*\([^)]*\)\s*\{[^}]*\}/g匹配函數(shù)體與本地代碼 diff。一旦發(fā)現(xiàn)變更立即觸發(fā)人工復(fù)核流程。5. 運(yùn)行時(shí)動(dòng)態(tài) patch用 AST 解析自動(dòng)適配 webpack 版本升級(jí)5.1 為什么不能只靠人工盯版本某東 H5 的 webpack 構(gòu)建頻率約為每周 2~3 次每次更新都可能改動(dòng)h5st函數(shù)名generateH5st→getH5stSign插入無(wú)意義的 dead codeif (false) { console.log(1); }干擾正則匹配將performance.now()替換為Date.now() Math.random()組合把CryptoJS.SHA256替換為 Web Crypto API 的crypto.subtle.digest(SHA-256, ...)。人工維護(hù)成本指數(shù)級(jí)上升必須引入自動(dòng)化解析能力。5.2 用 esprima-python 解析 AST定位核心表達(dá)式我們不依賴函數(shù)名而是基于 AST 節(jié)點(diǎn)特征定位查找CallExpression節(jié)點(diǎn)callee.name為performance且property.name為now查找CallExpression節(jié)點(diǎn)callee.name為CryptoJS且property.name為SHA256查找BinaryExpression節(jié)點(diǎn)operator為且左右操作數(shù)包含t、u、r、d、p等變量名。安裝依賴pip install esprima-pythonPython AST 解析腳本精簡(jiǎn)核心邏輯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 # 遞歸遍歷子節(jié)點(diǎn) for child in ast.iter_child_nodes(node): traverse(child) traverse(tree) return result # 使用示例傳入從 Sources 面板復(fù)制的格式化 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參數(shù)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 函數(shù)不再是硬編碼而是由 AST 解析器動(dòng)態(tài)組裝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 上線前的三道驗(yàn)證關(guān)卡光有動(dòng)態(tài)生成不夠必須加驗(yàn)證閉環(huán)本地一致性驗(yàn)證用抓包得到的真實(shí)h5st值與本地 Python 函數(shù)輸出比對(duì)100% 一致才允許提交線上灰度驗(yàn)證新版本發(fā)布后將 5% 流量路由到新h5st生成邏輯監(jiān)控403率是否突增降級(jí)熔斷當(dāng)連續(xù) 3 次請(qǐng)求h5st失效自動(dòng)回退到上一版函數(shù)并告警通知。我一般會(huì)在generate_h5st()函數(shù)開(kāi)頭加一行assert len(h5st) 64 and all(c in 0123456789abcdef for c in h5st), fh5st format error: {h5st}從那以后我每次更新某東爬蟲(chóng)都強(qiáng)制走一遍本地抓包 → AST 解析 → 模板生成 → 一致性斷言 → 灰度上線的完整鏈路再也沒(méi)因?yàn)閔5st失效半夜被 call 起來(lái)救火。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取