简介:面向爬虫与前端逆向学习者的实战代码包,聚焦某东平台基于webpack方式打包的H5ST签名算法,重点解决采集过程中加密参数生成与校验难题。压缩包共2个文件,包含1个Python脚本与1个JavaScript文件,其中JS文件对应webpack模块加载、算法核心逻辑及参数拼接过程,Python脚本用于补全浏览器环境、调用JS接口并输出最终签名,整体仅159KB,轻量易用。目前已有852人学习下载,适合具备一定JS逆向基础、希望快速掌握webpack打包特征与H5ST生成流程的开发者。通过该代码可直观理解如何从webpack模块中定位加密入口、还原函数调用关系、构造合法请求参数,并可直接对接爬虫框架提取数据;学习后可大幅减少自行调试与逆向分析的时间,快速落地实际项目。两个文件虽少,但覆盖了从环境补齐、签名生成到参数使用的完整闭环,对正在研究某东反爬机制或webpack逆向的读者具备较高参考价值。
1. 入手webpack方式的h5st逆向破解:一条能摆脱浏览器壳子的路
某电商平台的h5st签名参数,在2024年之后几乎成了所有自动化采集脚本的拦路虎。你带齐了cookie和UA,接口照样返回sign error。缺的就是网页里那段webpack打包出来的JS在运行时生成的h5st。别急着上无头浏览器,杀内存不说,指纹还容易漂。更稳的做法,是把页面里webpack打包的算法工程整个搬到Node.js,补上缺失的浏览器环境,让函数在本地直接产出签名。这条路线就是webpack方式的h5st逆向破解。这里给出一套可复现的完整代码链路:从抓包定位、扣bundle、补环境、导出算法函数,到自研签名替换与回归验证。适合已经会抓包,想把手动操作变成服务化接口的前端、自动化采集工程师。
2. 定位h5st签名链路:从抓包到选型,为什么webpack路线更适合生产
2.1 先抓三组请求,确认h5st的输入输出
我接到一个具体需求的时候,不会一上来就下载网页JS。第一步永远是抓包。用代理工具把目标接口的完整链路记录下来,至少要抓三组:间隔10秒的一组、间隔5分钟的一组、换登录账号后的一组。抓包的时候同时把页面HTML保存下来,后面提取script会用到。
观察点有三个。第一,h5st出现在哪个位置,是URL query、请求头还是表单体。第二,在接口参数不变的情况下,隔一段时间h5st是否变化;如果变化,说明里面带了时间戳或随机数。第三,换账号之后h5st长度和前缀有没有变化,判断里面是否绑定了token或用户指纹。
为了让样本留得规整,我会用mitmproxy挂一个拦截脚本,把带h5st的请求连同完整请求头落盘成JSONL,方便后面做回归样本。
# h5st_capture.py # mitmproxy 插件:拦截带 h5st 的请求,把现场完整落盘 import json from mitmproxy import http def request(flow: http.HTTPFlow) -> None: if "h5st" not in flow.request.query: return record = { "url": flow.request.url, "method": flow.request.method, "headers": dict(flow.request.headers), "timestamp": flow.request.timestamp_start } with open("samples/h5st_sample.jsonl", "a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n")这段脚本只做拦截和落盘,不做任何改写。flow.request.query是mitmproxy里请求query参数的容器,判断“h5st”是否在参数里,比直接搜URL字符串更可靠,能避免把路径里其他同名片段误判成签名。timestamp_start是请求进入代理的时刻,单位是秒,后面换算成毫秒对比时间戳时记得乘以1000。
这一步做完,你会得到三组完整的请求样本:完整URL、请求头、请求体、页面HTML。这些样本是后续回归验证的基线。我一般会按samples/20250110_1.json这种方式归档,避免后面验证时找回不了当时的现场。
从抓包结论看,某东的h5st出现在商品详情、价格、搜索等关键接口的query参数里,由页面JS在请求发起前同步生成。服务端拿到请求后,会先校验h5st再返回业务数据。只要h5st不对,无论cookie多完整都是白搭。
2.2 三种常见实现路线,为什么我选webpack补环境
处理h5st这类页面签名,市面主流是三条路:AST还原算法、无头浏览器执行、webpack补环境执行。我一个个说下成本。
AST还原是把混淆的加密算法直接还原成可读代码,优点是没有运行时依赖,缺点是工作量大、周期长,而且某东的h5st核心算法在每次版本更新后都可能调整,还原一次的成本很高。无头浏览器方案是用playwright或puppeteer直接加载页面、触发请求、截获h5st,优点是实现快,缺点是内存占用高,并发一上来机器就吃紧,而且指纹环境每次都要重新生成。
我自己的选择是webpack补环境。原因很简单:h5st的算法本身就是webpack模块,只要把webpack的运行时搬到Node.js,让它在模块加载时认为自己在浏览器里,那么生成h5st的函数就能被直接调用。这个方案的优点是保留了原始算法逻辑,不需要还原每一行混淆代码,资源占用和无头浏览器比低不少。
下面是三个方案的对比,方便你拍板:
| 实现路线 | 开发工作量 | 运行时资源 | 版本更新后的维护成本 | 适合场景 |
|---|---|---|---|---|
| AST还原算法 | 高 | 低 | 高(每次版本要重新分析) | 算法长期不变 |
| 无头浏览器执行 | 低 | 高(每个并发一个浏览器实例) | 低 | 小批量、临时验证 |
| webpack补环境 | 中 | 低(纯Node进程) | 中(需跟随chunk结构变化) | 服务化接口、批量采集 |
结论是:如果你要做的是长期稳定服务,而不是临时跑一两个请求,webpack补环境是性价比最高的入口。后面所有代码都是按这个路线展开的。
2.3 在webpack打包产物里定位h5st算法模块
某东的前端工程是典型的webpack多chunk结构。打开页面源码能看到一堆script标签,主bundle先执行,h5st相关的算法往往被放到独立的chunk里,通过jsonp异步加载。这是webpack打包优化配置里splitChunks的常规做法:把体积大且不常改的业务模块拆开,让首屏只加载必要代码。
找算法模块的办法是搜特征字符串。用编辑器或者命令行把下载好的JS全部搜一遍,优先搜“h5st”这个关键词,通常能命中变量名、字符串常量或注释。命中后用括号匹配把包含算法函数的整个模块块看一遍。webpack 4时代的模块签名形如:
"h5st_algorithm_id": (function(module, exports, __webpack_require__) { // 这里是加密算法实现 })如果搜“h5st”命中太多,再叠加搜索“generate”“sign”“md5”“sha”这类关键词缩小范围。实际操作里,h5st那部分代码往往和token、timestamp拼接是同一个模块,找到之后把模块id记下来,后面注入导出代码时要用它。这里有个很玄学的经验:有些压缩后的bundle里“h5st”字符串会出现几十次,但真正的算法函数名可能被替换成_0xabc123这种形式,所以别只看函数名,要顺着字符串常量的引用关系往上找。
3. 把webpack bundle搬到Node.js:扣代码、补环境、导出函数的完整代码
3.1 从页面HTML里下载全部webpack bundle
这一步的目标,是把页面上所有外链JS下载到本地。我习惯写一个一次性脚本,直接读保存的HTML文件,正则提取script src,然后批量下载。
下面这个download.js,我在多个项目里复用,改下页面路径就能跑。
// download.js // 从本地html提取script src并批量下载webpack bundle到./bundle目录 const fs = require('fs'); const path = require('path'); const https = require('https'); const http = require('http'); const html = fs.readFileSync('./samples/page.html', 'utf-8'); const scriptRex = /<script[^>]+src="([^"]+)"/g; const srcList = []; let match; while ((match = scriptRex.exec(html)) !== null) { const url = match[1]; if (url.startsWith('http') || url.startsWith('//')) { srcList.push(url.startsWith('//') ? 'https:' + url : url); } } function download(url, outPath) { const mod = url.startsWith('https') ? https : http; return new Promise((resolve, reject) => { const options = { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', 'Referer': 'https://item.jd.com/' } }; mod.get(url, options, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { fs.writeFileSync(outPath, Buffer.concat(chunks)); console.log('saved:', outPath); resolve(outPath); }); }).on('error', reject); }); } (async () => { fs.mkdirSync('./bundle', { recursive: true }); for (const url of srcList) { const fileName = './bundle/' + path.basename(url.split('?')[0]); await download(url, fileName); } console.log('done, total files:', srcList.length); })();这段代码做了三件事:读HTML、正则抽出所有外链script、逐个下载到bundle目录。两个参数建议注意一下。headers里的Referer必须填目标页面域名,否则部分CDN直接403;User-Agent最好和你抓包时保持一致,因为某些时段服务端会按UA下发不同版本的chunk。文件名直接用URL里的basename,去掉query参数,避免问号造成路径问题。
下载完成后,检查一下bundle目录里的文件数量。正常情况下主bundle之外的几个chunk也都会在这里。如果某个文件特别小,只有几KB,那可能是入口脚本,不要漏掉。如果页面里有一堆第三方埋点脚本,可以只挑主bundle和h5st相关chunk放进后续加载列表,减少补环境时遇到的无关依赖。
3.2 用vm沙箱补环境,把webpack运行时跑起来
bundle文件对浏览器全局环境有强依赖。直接扔进Node的global里跑,第一行就会报window is not defined。我采用vm模块建沙箱,把常见的浏览器全局对象先塞进去,然后在沙箱里执行bundle代码。
下面是node-runner.js的骨架,最小到能把webpack运行时立起来。
// node-runner.js // 最小补环境:让webpack bundle在vm沙箱里认为自己在浏览器中 const fs = require('fs'); const vm = require('vm'); const files = [ './bundle/main.bundle.js', './bundle/h5st.chunk.js' ]; const sandbox = { console, setTimeout, clearTimeout, setInterval, clearInterval, Buffer, location: { href: 'https://item.jd.com/100012043624.html', host: 'item.jd.com', protocol: 'https:' }, navigator: { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', platform: 'Win32', language: 'zh-CN', languages: ['zh-CN', 'zh', 'en'], vendor: 'Google Inc.' }, document: { cookie: '', referrer: '', createElement() { return { tagName: 'canvas', getContext: () => ({ measureText: () => ({ width: 0 }) }), toDataURL: () => '', getBoundingClientRect: () => ({ width: 0, height: 0 }) }; } }, localStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {} }, sessionStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {} } }; sandbox.window = sandbox; sandbox.self = sandbox; sandbox.globalThis = sandbox; vm.createContext(sandbox); for (const file of files) { const code = fs.readFileSync(file, 'utf-8'); vm.runInContext(code, sandbox, { filename: file }); console.log('loaded:', file); } console.log('webpackJsonpPush:', typeof sandbox.webpackJsonpPush); module.exports = { sandbox };几个关键点。sandbox.window = sandbox要在createContext之前做,这样bundle里的window、self、globalThis都指向同一个对象,否则“window is not defined”和“self is not defined”会轮番出现。document.createElement返回的对象必须带getContext和measureText,因为指纹算法会真的去调用canvas,一旦返回undefined,后续代码只能崩。localStorage和sessionStorage用最简单的桩实现,先把代码跑通,真实值后面再补。
如果最后一行打印出的webpackJsonpPush类型是function,说明webpack运行时已经在沙箱里就位。这时候还没法直接调用h5st,因为算法模块可能还没执行,或者执行了但没暴露到全局。下一步就是注入调试模块把函数导出来。
3.3 注入调试入口,把h5st生成函数导出为可调用函数
webpack的jsonp运行时暴露了一个入口:webpackJsonpPush。它的签名是(chunkIds, moreModules, executeModules),我们借助它注册一个自己的调试模块,在模块代码里拿到__webpack_require__,进而去require h5st算法模块,再把结果挂到沙箱的globalThis上。
下面是inject-h5st.js的核心注入代码。
// inject-h5st.js // 在vm沙箱里注入调试模块,把h5st算法函数暴露到globalThis const vm = require('vm'); const { sandbox } = require('./node-runner'); const injectCode = ` (function() { var debugModules = {}; debugModules['debug_h5st'] = function(module, exports, __webpack_require__) { var h5stModule = __webpack_require__('h5st_algorithm_id'); globalThis.__genH5st = function(input) { return h5stModule.generate(input); }; }; webpackJsonpPush([], debugModules, [['debug_h5st']]); })(); `; vm.runInContext(injectCode, sandbox); if (typeof sandbox.__genH5st === 'function') { console.log('__genH5st ready'); } else { console.error('inject failed, check module id or load order'); }三个参数的含义是:第一个空数组表示这个调试模块不属于任何已有chunk;第二个是模块表,键就是模块id;第三个是要在模块加载后立即执行的模块id列表。webpack运行时收到这个push后,会把debug_h5st模块放进modules表,然后执行它。模块函数三个参数里,__webpack_require__就是webpack内部的require,用它去加载真实的h5st算法模块。
注意,这里的“h5st_algorithm_id”必须替换成你在2.3节里定位到的真实模块id。如果id是数字,就写数字不用引号,或者在数字外包一层String转换。inject之后如果打印出来的是function,说明导出成功。如果报module not found,多半是目标模块在那个chunk还没加载,检查files数组里有没有包含它对应的chunk文件。
导出的__genH5st此时已经具备生产签名能力,可以先手动调用一次,入参用抓包请求里看到的token、时间戳等信息。调用成功后再进入下一步的算法还原与替换。
4. 还原签名算法:源串拼接规则、必调参数与自研替换代码
4.1 hook算法入口,把入参出参记录成回归样本
有了可调用的__genH5st之后,第一件事不是去分析它内部怎么写,而是记录它每次被调用时的输入输出。这里在导出函数外再包一层recorder。下面这段hook-recorder.js代码,会把最近200次调用的参数和结果缓存在内存里,同时打印到控制台。
// hook-recorder.js // 在原算法外做参数记录,保留最近200条调用日志 function createRecorder(realFn) { const records = []; return function recorder(...args) { const startedAt = Date.now(); const result = realFn.apply(this, args); const entry = { invokeTime: startedAt, args: args.map(a => { if (a === null || a === undefined) return String(a); if (typeof a === 'object') return JSON.stringify(a); return String(a); }), result: typeof result === 'string' ? result : JSON.stringify(result) }; records.push(entry); if (records.length > 200) records.shift(); console.log('h5st invoke:', entry.invokeTime, '->', entry.result.slice(0, 40)); return result; }; } // sandbox 来自 inject-h5st.js 执行后的上下文 sandbox.__genH5st = createRecorder(sandbox.__genH5st);这里把入参做了序列化,对象会转成JSON字符串,普通类型转成原始字符串。result只打前40个字符,避免输出太长刷屏。拿到一批调用记录后,用抓包样本和这些日志对齐,就能确认几个关键问题:h5st是否依赖时间戳(相同入参在不同时间调用结果是否不同)、是否依赖指纹(换指纹结果是否变化)、签名长度和格式是否稳定。
对齐的方式很简单:把抓包样本里请求发出时的参数原样传给__genH5st,如果输出和线上h5st一致,说明环境已经完整;如果不一致,优先检查当前沙箱里的时间戳、指纹和线上生成时是不是同一套。这一步的产出是一张入参出参对照表,后面还原源串时全靠它。
4.2 签名源串的通用规律与参数调优表
做过多个电商平台签名逆向之后,你会发现这类签名参数的生成逻辑高度相似:取一批易变字段,按固定顺序拼接,再做一次摘要算法,最后加上时间戳前缀。h5st的核心就落在源串拼接顺序和摘要算法上。
下面是h5st这类参数里最常见的参与字段,以及它们各自的行为属性:
| 参数名 | 数据来源 | 变化频率 | 是否必调 | 说明 |
|---|---|---|---|---|
| appId | 站点固定标识 | 基本不变 | 必调 | 用于区分业务线,参与源串 |
| timestamp | 页面发起请求前取服务器时间 | 秒级变化 | 必调 | 签名里通常直接带明文时间戳 |
| token | 登录态下发的令牌 | 数小时到数天 | 必调 | 绑定用户身份,参与源串 |
| fingerprint | 页面指纹算法生成 | 每次会话可能变化 | 高度影响 | 最不稳定,建议整体导出指纹生成函数 |
| businessParams | 当前接口业务参数 | 每个请求都不同 | 选调 | 按key排序后参与拼接 |
调优时最需要注意的是timestamp。我一般在签名生成时不用本地Date.now(),而是从页面接口的响应头或页面初始化数据里取服务器时间,避免本地时钟偏差导致服务端校验失败。fingerprint则不建议手工固定,因为同一账号换环境后指纹变化是弱风控信号,反而容易被关注。
4.3 完整代码里的替换方案:用自研签名函数换下原算法
在已经能用原算法生产合法h5st的前提下,如果业务方提出要把生成逻辑沉淀成自己的服务,就可以考虑自研替换。实现方法是用原生Node加密模块重写hash过程,替换掉webpack里那段混淆代码。这里给一个可运行的示意实现。
// self-sign.js // 自研签名替换:拼接规则按抓包样本反推,hash算法按实际样本确认 const crypto = require('crypto'); function buildSign({ appId, timestamp, token, fingerprint, businessParams }) { const sortedQuery = Object.keys(businessParams) .sort() .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(businessParams[k])) .join('&'); // 示例顺序,真实顺序务必以抓包反推为准 const source = [appId, timestamp, token, fingerprint, sortedQuery].join('&'); return crypto.createHash('sha256').update(source, 'utf-8').digest('hex'); } function genH5st(input) { const timestamp = input.timestamp || Date.now(); const sign = buildSign({ ...input, timestamp }); return timestamp + '_' + sign; } module.exports = { genH5st };这段代码能不能直接跑通取决于一件事:buildSign里的source拼接顺序和hash算法,是否与你从样本中反推出来的一致。反推方法是控制变量——固定其他字段只改token,观察输出是否变化;再固定token只改businessParams的某个键,再观察变化。这样逐字段试下来,就能把源串的构成顺序试出来。
自研替换的好处是彻底摆脱了webpack bundle的依赖,代码体积小、可读性强、便于服务化部署。风险是需要保持和平台版本同步;一旦平台改了签名算法,你需要重新跑一轮反推。我一般会在自研替换代码外面保留原算法调用入口,作为版本升级时的对照基准,这个习惯帮我省过很多排查时间。
4.4 一份“完整代码”工程到底包含哪些文件
很多人问“完整代码”是不是指把整个webpack bundle脱敏后贴出来。不是。工程意义上的完整代码,是让一个没接触过这个项目的人,按步骤拉下来就能跑通整条链路的一套工程文件。一个标准目录应该是这样的:
h5st-reverse/ download.js # 下载页面bundle node-runner.js # 补环境并加载bundle inject-h5st.js # 注入调试入口,导出算法函数 hook-recorder.js # 记录算法入参出参 self-sign.js # 自研签名替换实现 verify.js # 回归验证脚本 samples/ # 抓包样本,至少三组实际交付时,我会把bundle目录和抓包样本目录单独放,不进版本库,因为bundle体积大而且会随版本更新。真正进版本库的是download.js、node-runner.js、inject-h5st.js、self-sign.js和verify.js这五个脚本文件,配合README里的抓包说明,一个新的同事照着流程走,一天之内能跑通。
5. 避坑清单:webpack方式跑h5st最容易翻车的7个细节
下面这7条是我在h5st这个项目上攒下来的血泪经验,按遇到频次排序,每一条都是真实翻过车的。
5.1 现象:Node里跑bundle第一行就报window is not defined
原因:webpack bundle的顶层代码在初始化阶段就开始访问window,用来做环境探测。Node原生没有window,所以第一行就崩。
解决:在构建sandbox时提前把window挂到sandbox自身,而且必须在vm.createContext之前完成。注意window、self、globalThis要指向同一个对象,三者缺一都会在后续代码里以不同名称再崩一次。
sandbox.window = sandbox; sandbox.self = sandbox; sandbox.globalThis = sandbox; vm.createContext(sandbox);5.2 现象:navigator或canvas相关调用返回undefined,指纹计算直接中断
原因:指纹算法会调用canvas的getContext、measureText、toDataURL,还会读取字体列表和屏幕参数。桩对象如果没按调用方期望的结构实现,后面的属性访问会抛TypeError。
解决:canvas的桩不能只返回一个空对象。getContext要返回带measureText方法的上下文,measureText要返回带width属性的对象;toDataURL要返回字符串。这样指纹算法至少能走完调用链,即使数值不是真实值,也不至于中断。如果页面代码里还调用了document.createElement('div'),统一返回同一个对象一般也不会出问题,因为业务代码通常只操作style和innerHTML这类属性。
5.3 现象:webpackJsonpPush执行了,但导出的算法函数是undefined
原因:目标算法模块不在主bundle里,而在后续通过jsonp加载的chunk中。files数组漏掉了对应chunk,或者模块id写错。
解决:先确认bundle目录下所有文件都已加载,再检查注入代码里的模块id和2.3节定位到的是否一致。如果id是数字,要转成数字传入而不是字符串。还有一个土办法:在注入模块里用Object.keys(webpack_require)打印一下已注册的模块id列表,对照列表确认目标id的写法。
5.4 现象:本地生成的h5st偶尔能过校验,偶尔被拒绝
原因:签名里带了timestamp,但本地用Date.now()生成,和请求发出时的服务器时间差了几秒。服务端一般允许的窗口很小,超了就拒绝。
解决:把timestamp改成请求发出前从服务端获取的时间,或者从接口响应头的date字段取时间。签名生成时间要和请求发出时间尽量贴近,差一两秒以内最稳妥。如果拿不到服务端时间,至少保证Node服务器的系统时钟和北京时间同步,并在启动命令里设置TZ=Asia/Shanghai。
5.5 现象:签名结构看起来一致,但服务端就是校验失败
原因:指纹值不真实。很多指纹算法会读取cookie里的设备标识、localStorage里的历史值,直接用字符串常量替代会破坏签名源串。
解决:把指纹生成函数和h5st生成函数一起导出,而不是只导h5st。让指纹函数自己在沙箱里跑一遍,把过程中读取的cookie键、localStorage键全列出来,再对照浏览器真实环境补齐。这一步比较繁琐,但绕不过去,指纹是h5st里最影响校验结果的部分。
5.6 现象:本地跑得好好的,一换服务器就报各种is not defined
原因:新服务器上Node版本、系统架构不同,webpack bundle里可能用到了Buffer等Node全局类型,或者依赖了时钟、时区设置。
解决:在node-runner.js里显式传入Buffer、setTimeout等Node全局对象到sandbox。时区问题建议在启动命令里统一设置TZ=Asia/Shanghai,避免时间偏移影响签名。换机器后先跑一遍verify.js,不要直接上生产流量。
5.7 现象:所有请求都成功,但一段时间后账号被风控限制
原因:签名本身没被识破,但请求频率、指纹复用和业务行为暴露了自动化特征。签名逆向解决的是参数合法性,解决不了行为风控。
解决:控制单位时间请求量,让指纹、token、UA、IP保持一致来绑定环境,并及时更换新指纹。这块没有银弹,只能自己摸索平台容忍阈值。遇到风控先停半小时,查指纹新鲜度和请求频率,而不是盲目加并发。
6. 回归验证三件套:把本地签名和线上样本钉死
6.1 在抓包样本上跑回归
我把抓包存档的三组请求整理成三条case,分别记录接口URL、完整参数、线上h5st。verify.js每次改动后都会跑这三条case,对比本地生成的h5st和线上h5st的差异。
| 样本 | 线上h5st前缀 | 本地h5st前缀 | timestamp差值 | 是否一致 |
|---|---|---|---|---|
| case1 | a3f2... | a3f2... | 1s | 一致 |
| case2 | 8c91... | 8c91... | 0s | 一致 |
| case3 | f5b0... | f5b0... | 1s | 一致 |
如果前缀一致、timestamp差在2秒内,说明签名链路基本可信。前缀不一致,则优先检查指纹和拼接顺序。
6.2 把自研签名接进请求管道
用自研签名替换后,请求管道里只需要一行调用。我习惯把token、fingerprint从登录态里取出来,统一放到一个session对象里,请求发出前从这里拿值算签名。
// http-client.js const { genH5st } = require('./self-sign'); function buildRequest(url, businessParams, session) { const timestamp = Date.now(); const h5st = genH5st({ appId: session.appId, timestamp, token: session.token, fingerprint: session.fingerprint, businessParams }); return { url: url + '?h5st=' + encodeURIComponent(h5st), headers: { 'User-Agent': session.userAgent, 'Cookie': session.cookie } }; }这段代码的要点是让timestamp尽可能靠近真实请求时间,并且在session里持久化token和fingerprint。注意不要每请求都重新生成指纹,那样会造成指纹和cookie绑定关系跳变,反而触发风控。
6.3 边界意识:签名能过,不代表行为合法
这套代码我只建议用在自己有权限的合法采集场景里。签名能过参数校验,只是解决技术门槛,不意味着可以无节制请求。我自己一般会让业务侧监控同一套token下每小时的成功率,一旦出现明显的成功率滑坡,先停半小时查指纹新鲜度和请求频率,而不是盲目加并发。逆向签名这条路,做成服务容易,做成稳定服务难,难就难在边界意识。希望帮到你。
本文还有配套的精品资源,点击获取