Python逆向京东联盟h5st 3.1签名参数:从JS混淆到数据采集实战
1. 项目概述:当爬虫遇上京东联盟的“铜墙铁壁”
做数据采集的朋友,尤其是关注电商联盟数据的,这两年肯定没少被京东联盟的h5st参数折腾。这玩意儿就像是京东给自家数据大门加装的一道动态密码锁,而且这锁的算法还在不断升级。我最近刚啃下h5st 3.1这个硬骨头,整个过程可以说是“道高一尺,魔高一丈”的典型攻防战。简单说,h5st是京东联盟H5页面接口中一个核心的签名参数,它由多个字段加密生成,每次请求都必须携带且唯一有效,直接关系到你能否成功拿到商品、订单、佣金这些关键数据。如果你的爬虫脚本里这个参数不对,或者过期了,服务器立马给你返回一堆错误码,数据?想都别想。
这次实战的目标很明确:在不依赖浏览器自动化(比如Selenium)这种笨重、低效方式的前提下,纯粹用Python逆向分析出h5st 3.1参数的生成逻辑,并实现本地化计算。这意味着我们要深入JavaScript混淆代码的腹地,把那些被压缩、加密、打乱的算法逻辑给捋顺了,然后用Python复现出来。这不仅仅是写个爬虫那么简单,更像是一次小型的密码学工程,涉及JS逆向、算法还原、Python加密库应用等一系列技能。适合谁看呢?如果你是对电商数据有需求的开发者、对JS逆向感兴趣的学习者,或者正在被类似签名参数困扰的爬虫工程师,那这篇从实战踩坑到最终实现的完整记录,应该能给你提供一条清晰的路径和不少避坑指南。
2. 核心思路与逆向突破口选择
面对h5st这种级别的反爬,一头扎进海量的混淆JS里是最蠢的做法。我的核心思路是“由外而内,定点爆破”。首先,得搞清楚h5st参数在哪个环节被生成并加入到网络请求中。最直接有效的方法就是使用浏览器的开发者工具(DevTools)进行网络抓包。
打开京东联盟的H5页面(例如某个商品推广页),在Network(网络)面板中,筛选XHR或Fetch请求,重点关注那些返回数据是商品详情、订单列表的接口。你会发现,这些请求的Headers里或者Payload里,大概率会有一个名为h5st的参数,它是一长串看起来毫无规律的字符。这就是我们的目标。接下来,不是直接去搜索h5st,而是利用DevTools的“Initiator”(发起者)标签页,或者更强大的“Search in files”(在文件中搜索)功能,去查找生成或包含这串字符的JavaScript代码片段。
这里有个关键技巧:h5st参数通常不是凭空产生的,它往往是对当前页面环境、用户行为、时间戳等一系列参数进行加密计算的结果。因此,在搜索时,可以尝试搜索h5st参数值中的某一段(比如前8位),或者搜索可能参与计算的参数名,如body、functionId、appid、t(时间戳)、fp(指纹)等。一旦定位到疑似生成函数,就要开始枯燥但至关重要的代码分析工作了。此时的代码通常是经过obfuscator等工具混淆的,变量名都是a, b, c, d,函数调用层层嵌套。我们的突破口就是找到最核心的加密函数入口,然后通过“跟栈”(Follow the call stack)的方式,一步步理清它的输入、输出和内部处理逻辑。
注意:京东的JS混淆和反调试手段可能会定期更新。你可能会遇到“无限debugger”、代码动态加载、环境检测等障碍。这时候需要配合使用“禁用断点”、“重写函数”或者一些浏览器插件来绕过。记住,我们的目的是理解算法,不是和反调试机制死磕,必要时可以“黑盒”补环境,优先保证能追踪到数据流。
3. 逆向环境搭建与关键工具链
工欲善其事,必先利其器。纯靠肉眼和浏览器控制台去硬啃混淆代码,效率极低且容易出错。一套高效的逆向工具链能让你事半功倍。
1. 浏览器与开发者工具:主力依然是Chrome或Edge的DevTools。除了基本的断点(Breakpoint)、监控网络请求,要特别熟练使用“Call Stack”(调用堆栈)面板和“Scope”(作用域)面板。当你在加密函数入口处打上断点,发起一个请求时,调用堆栈会清晰展示出是哪些函数一步步调用了这个加密函数。而作用域面板则能让你在断点暂停时,查看当前函数局部变量、闭包变量以及全局变量的具体值,这是理解参数来源和中间计算结果的黄金窗口。
2. JS代码格式化与解混淆工具:从Sources(源代码)面板直接拷贝出来的JS代码往往是被压缩成一行的。你需要一个强大的格式化工具。Chrome DevTools自带的“Pretty print”(美化打印,那个{}图标)是基础。对于更复杂的混淆,可以使用像jsnice、prepack.io或de4js这样的在线工具或本地Node.js工具(如javascript-obfuscator的反向工程尝试)。它们能尝试重命名变量、解析简单的控制流平坦化,让代码可读性大幅提升。但别完全依赖自动化工具,核心逻辑的梳理最终还得靠人脑。
3. Python侧的核心库:当我们把JS算法逻辑搞清楚后,就要在Python里复现。这离不开几个库:
requests: 用于最终构造和发送HTTP请求。execjs或PyExecJS: 这是关键。有时JS算法过于复杂,完全用Python重写成本太高。这两个库允许你在Python环境中直接调用JavaScript代码片段或文件。你可以把关键的、难以移植的加密函数抠出来,放在一个.js文件里,然后用execjs去调用它,传入参数并获得h5st值。这是一种“半自动化”的讨巧方案,稳定且快速。nodejs环境:execjs通常需要一个JS运行时环境,安装Node.js是最常见的选择。- 纯Python加密库:如果决定完全用Python重写,那么
hashlib(用于MD5、SHA等哈希)、hmac、json、time、random、Crypto(用于AES、RSA等)等标准库或第三方库pycryptodome就是必备的。你需要根据逆向出的算法,精确地使用对应的Python函数进行等价实现。
4. 调试与比对工具:在逆向过程中,需要不断对比。用浏览器正常访问一次,抓取到正确的h5st(我们称之为“标准答案”)。然后,在你用Python(或execjs调用)计算出一个h5st后,需要和“标准答案”进行逐位比对。如果不一样,就要回头检查是哪个输入参数不对,还是哪一步计算出了偏差。print大法、日志记录,以及将中间变量输出与浏览器Debug时看到的值进行比对,是调试阶段的核心手段。
4. h5st 3.1 参数生成逻辑深度拆解
通过逆向分析,我们可以将h5st 3.1的生成逻辑抽象为几个核心阶段。请注意,以下流程是基于通用模式的分析,具体细节可能因京东的迭代而微调,但整体框架具有很高的参考价值。
4.1 参数收集与预处理阶段
h5st不是凭空产生的,它是对一系列“原料”进行加工后的产物。在发起一个API请求前,客户端(浏览器)会收集以下关键信息:
- 固定参数:如
functionId(标识接口功能)、appid(应用标识)、client(客户端类型)、clientVersion(客户端版本)等。这些通常在页面加载的全局变量或特定JS对象中定义。 - 动态参数:如
t(当前时间戳,精确到毫秒)、body(请求体,通常是JSON字符串,可能包含商品ID、页码等信息)、uuid或fp(设备或浏览器指纹,通过Canvas、WebGL等多种方式生成,具有唯一性和一定稳定性)。 - 用户上下文参数:如
cookie中的关键字段(例如pin、wskey等,用于标识用户身份),这些在计算签名时可能会被间接使用。
预处理工作包括:将body从对象转换为排序后的JSON字符串(有时需要按字母顺序排序键名),将时间戳t转换为字符串,以及其他参数的字符串拼接准备。一个常见的坑是JSON序列化时的空格和键序问题。Python的json.dumps默认会有些微空格,且键序在3.7以下版本不固定,必须与JS端的JSON.stringify行为严格一致,通常需要指定separators=(‘,’, ‘:’)来去除空格,并确保字典(对象)的键在传入dumps前就已按字母顺序排列好。
4.2 核心加密算法逆向与还原
这是最核心、最困难的一步。通过断点和代码跟踪,你会发现收集到的参数被送入一个或多个加密函数中。h5st 3.1的算法通常不是单一的MD5或SHA,而可能是多层嵌套的哈希(如先SHA256再取部分)、HMAC,或者是自定义的混淆算法(比如将字符串与一个秘钥进行循环XOR,再进行Base64编码等)。
逆向时,你需要紧盯几个关键点:
- 入口函数:找到最终输出
h5st字符串的那个函数。给它打上断点,观察其输入参数。 - 参数流转:在作用域面板里,记录下所有传入参数的值。然后一步步“Step into”(步入)这个函数,观察参数如何被处理、拼接、转换。
- 识别标准算法:注意代码中是否有类似
CryptoJS.MD5(...).toString()、window.btoa(...)、createHmac(‘sha256’, key)等调用。这些是标准加密库的用法,很容易在Python中找到对应实现。 - 处理自定义算法:如果遇到一长串位运算(
&,|,<<,>>>)、数组循环操作,那很可能是自定义算法。你需要耐心地将其逻辑翻译成Python。这里execjs的优势就体现出来了:如果这个自定义函数不太长但逻辑绕,你可以直接把整个JS函数抠出来,让execjs去执行,省去翻译的麻烦和出错风险。 - 盐值(Salt)与密钥:特别注意算法中是否硬编码了某个字符串或数字(盐值),或者从某个全局变量、函数返回值中获取了密钥。这些是签名有效性的关键,必须完全还原。
4.3 最终拼接与输出格式
经过核心加密后,得到的可能是一个十六进制字符串或Base64字符串。但h5st的最终格式往往不是单纯的密文。观察抓包得到的h5st值,你可能会发现它是由几部分通过特定的分隔符(如;)连接而成的。一个典型的h5st 3.1格式可能是:加密结果;时间戳;随机数;其他标识...。
例如,可能是:a1b2c3d4e5f6;1640995200000;123456;1.0。这意味着,客户端不仅发送了加密签名,还把生成签名所用的时间戳、一个随机数(防止重放攻击)、版本号等也一并发送了,服务端会用同样的逻辑和这些明文参数进行验签。在Python复现时,你必须严格按照这个格式进行最终拼接。
5. Python复现实战:从零到一生成有效h5st
理论分析完毕,我们进入实战编码环节。这里我提供两种主流的实现思路,并给出关键代码示例。
5.1 方案一:纯Python原生实现(推荐用于学习)
此方案要求已将JS加密算法完全翻译为Python函数。假设我们逆向出的算法是:对字符串S = functionId + ‘&’ + t + ‘&’ + JSON.stringify(sorted_body) + ‘&’ + secret_salt进行SHA256哈希,然后取前16位十六进制字符,再与时间戳t、随机数r以分号连接。
import hashlib import json import time import random def generate_h5st_v31(function_id, body_dict, secret_salt): """ 生成 h5st 3.1 参数 (纯Python模拟) :param function_id: 接口函数ID,如 ‘unionSearch’ :param body_dict: 请求体参数字典 :param secret_salt: 逆向得到的固定盐值 :return: 完整的h5st字符串 """ # 1. 生成时间戳和随机数 t = str(int(time.time() * 1000)) # 13位毫秒时间戳 r = str(random.randint(100000, 999999)) # 6位随机数 # 2. 处理body:按键名排序后序列化为紧凑JSON # 注意:必须确保键序与JS端一致。Python 3.7+ dict默认保持插入序,但为保险可显式排序。 sorted_body_str = json.dumps(body_dict, separators=(‘,’, ‘:’), sort_keys=True) # 3. 构造待签名字符串 (根据逆向逻辑调整拼接顺序和分隔符) sign_str = f“{function_id}&{t}&{sorted_body_str}&{secret_salt}” # 打印中间值便于调试 # print(f“待签名字符串: {sign_str}”) # 4. 计算SHA256并取前16位 sha256_hash = hashlib.sha256(sign_str.encode(‘utf-8’)).hexdigest() sign_part = sha256_hash[:16] # 5. 按格式拼接最终h5st h5st = f“{sign_part};{t};{r};3.1” # 假设版本标识为3.1 return h5st # 使用示例 if __name__ == ‘__main__’: # 这些参数需要从实际请求中捕获或根据页面分析得出 test_function_id = “unionOpenActivityRedpacketQuery” test_body = {“actId”: “1234567890”, “from”: “h5”} test_salt = “jd_common_salt_2023” # 示例盐值,实际需逆向获取 result = generate_h5st_v31(test_function_id, test_body, test_salt) print(f“生成的h5st: {result}”)5.2 方案二:Python + execjs 混合实现(推荐用于生产)
当JS算法极其复杂,包含大量环境依赖或难以翻译的自定义函数时,此方案更稳健。我们将核心JS函数保存为文件。
首先,创建一个h5st_core.js文件,内容是你从浏览器中提取并稍作整理(确保它自包含,不依赖未定义的浏览器全局对象)的加密函数:
// h5st_core.js // 这是一个高度简化的示例,真实函数可能非常复杂 function generateSign(functionId, t, bodyStr, secretKey) { // 这里可能是复杂的混淆算法,比如调用了CryptoJS,或者一堆位运算 // 假设我们这里是一个模拟的复杂操作 var combined = functionId + t + bodyStr + secretKey; // ... 一系列复杂的哈希、编码、变换操作 ... // 最终返回签名部分 return “a1b2c3d4e5f6”; // 示例返回值 } // 暴露一个主函数给Python调用 function getFullH5st(functionId, body, salt) { var t = Date.now().toString(); var r = Math.floor(Math.random() * 900000 + 100000).toString(); var bodyStr = JSON.stringify(body); var sign = generateSign(functionId, t, bodyStr, salt); return sign + “;” + t + “;” + r + “;3.1”; }然后,在Python中使用execjs调用它:
import execjs import json # 1. 读取JS文件 with open(‘h5st_core.js’, ‘r’, encoding=‘utf-8’) as f: js_code = f.read() # 2. 创建JS执行环境 ctx = execjs.compile(js_code) # 3. 准备参数并调用 function_id = “unionOpenActivityRedpacketQuery” body_dict = {“actId”: “1234567890”} secret_salt = “jd_common_salt_2023” # 需与JS文件内使用的保持一致 # 注意:execjs调用时,参数直接对应JS函数的形参 h5st = ctx.call(“getFullH5st”, function_id, body_dict, secret_salt) print(f“通过execjs生成的h5st: {h5st}”)实操心得:在生产环境中,强烈建议使用方案二(execjs)。原因有三:第一,京东的算法可能频繁微调,只需更新JS文件即可,Python主逻辑不用动;第二,避免了将复杂JS逻辑翻译成Python可能引入的细微错误;第三,性能上,一次编译(
execjs.compile)可重复调用,开销可接受。但务必注意JS代码的纯净性,移除或模拟对window、document等浏览器特有对象的依赖,这通常被称为“补环境”。
6. 请求构造与数据抓取完整流程
有了生成h5st的能力,我们就能构造出合法的请求了。整个过程可以封装成一个爬虫类,以下是关键步骤的代码示例和解释。
import requests import time import json from urllib.parse import urlencode class JdUnionH5Spider: def __init__(self, cookie_str): """ 初始化爬虫 :param cookie_str: 从浏览器拷贝的完整cookie字符串 """ self.session = requests.Session() self.headers = { ‘User-Agent’: ‘Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1’, # 使用移动端UA ‘Accept’: ‘application/json, text/plain, */*’, ‘Accept-Language’: ‘zh-CN,zh;q=0.9’, ‘Accept-Encoding’: ‘gzip, deflate, br’, ‘Content-Type’: ‘application/x-www-form-urlencoded; charset=UTF-8’, # 注意Content-Type ‘Origin’: ‘https://u.jd.com’, ‘Referer’: ‘https://u.jd.com/’, # 根据实际H5页面设置 ‘Cookie’: cookie_str # 关键:身份凭证 } self.session.headers.update(self.headers) # 初始化execjs环境(如果采用方案二) # self.ctx = execjs.compile(open(‘h5st_core.js’, ‘r’, encoding=‘utf-8’).read()) def _generate_h5st(self, function_id, body_dict): """ 内部方法:生成h5st参数。 这里以纯Python方案为例,实际可替换为execjs调用。 """ # 此处应调用上一节中的generate_h5st_v31函数,或execjs的call方法 # 需要逆向获取真实的secret_salt secret_salt = “YOUR_REVERSED_SECRET_SALT_HERE” return generate_h5st_v31(function_id, body_dict, secret_salt) def fetch_activity_list(self, page=1, page_size=20): """ 示例:获取联盟活动列表 """ function_id = “unionOpenActivityRedpacketQuery” # 构造请求体,参数需根据实际接口文档或抓包分析 body = { “actId”: “”, # 可能为空表示查询列表 “pageIndex”: page, “pageSize”: page_size, “from”: “h5” } # 生成h5st h5st_value = self._generate_h5st(function_id, body) # 构造最终请求参数 params = { “functionId”: function_id, “body”: json.dumps(body, separators=(‘,’, ‘:’)), # 注意:body参数本身也需要是JSON字符串 “appid”: “u-jd-h5”, # 需根据抓包确认 “client”: “apple”, # 需根据抓包确认 “clientVersion”: “12.0.0”, “t”: str(int(time.time() * 1000)), # 时间戳,需与h5st内嵌的一致或无关? “h5st”: h5st_value, # 注入我们计算好的h5st } # 重要:有些接口参数是放在URL查询字符串中,有些是放在POST的form-data里。 # 此处假设为GET请求,参数在URL中。 url = “https://api.m.jd.com/api” full_url = f“{url}?{urlencode(params)}” try: response = self.session.get(full_url, timeout=10) response.raise_for_status() # 检查HTTP错误 data = response.json() # 检查业务码,京东接口通常有‘code’字段,0表示成功 if data.get(‘code’) == 0: return data.get(‘data’, {}) else: print(f“接口请求失败: code={data.get(‘code’)}, msg={data.get(‘msg’)}”) return None except requests.exceptions.RequestException as e: print(f“网络请求异常: {e}”) return None except json.JSONDecodeError as e: print(f“响应解析异常: {e}”) return None # 使用示例 if __name__ == ‘__main__’: # 你的京东联盟H5页面Cookie MY_COOKIE = “pin=xxx; wskey=xxx; ...” spider = JdUnionH5Spider(MY_COOKIE) activity_data = spider.fetch_activity_list(page=1) if activity_data: print(json.dumps(activity_data, indent=2, ensure_ascii=False))关键点解析:
- Cookie是灵魂:
Cookie是维持登录态的关键,必须从已登录京东联盟H5页面的浏览器中获取。wskey、pin等是关键字段。Cookie会过期,需要维护更新机制。 - 参数一致性:注意
body参数在请求中通常需要被json.dumps成字符串,并且其格式(空格、键序)必须与生成h5st时使用的body字符串完全一致,否则服务端验签会失败。这是一个极高频的错误点。 - 请求方式与参数位置:仔细分析抓包,确定是
GET还是POST。GET请求参数在查询字符串(URL后),POST请求参数可能在form-data或x-www-form-urlencoded中。h5st参数可能放在headers里,也可能放在body或query中,需以实际抓包为准。 - 其他必要参数:
appid、client、clientVersion等参数看似固定,但也必须携带,且值需正确。它们可能也参与了h5st的生成,或者服务端会做校验。
7. 常见问题排查与稳定性优化策略
即使按照上述流程走通,在实际运行中也会遇到各种问题。下面是一个常见问题排查表:
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
返回“签名错误”、“参数无效”等错误码 | 1.h5st生成算法错误。2. 参与签名的参数与发送的参数不一致。 3. 时间戳 t不同步或格式不对。4. 盐值(secret)错误或已过期。 | 1.比对大法:在浏览器端执行一次正常请求,在加密函数入口处断点,记录下所有输入参数的精确值(字符串形式)。在Python端,打印出用于生成签名的每一个中间变量,进行逐字比对。特别注意JSON字符串是否有多余空格、换行,键序是否一致。 2.时间戳:确保生成 h5st时使用的t和请求参数中的t是同一个值(或符合其规则)。3.更新JS代码:算法可能已升级,重新抓取最新的JS文件进行分析。 |
返回“未登录”、“权限不足” | 1. Cookie失效或错误。 2. 请求头缺少必要字段(如 Referer)。3. IP或设备指纹被风控。 | 1.检查Cookie:手动在浏览器访问对应页面,确认Cookie是否有效。检查wskey是否过期(需要定期刷新)。2.补全Headers:将浏览器抓包中的所有 Headers(特别是Origin,Referer,User-Agent)原样复制到爬虫中。3.模拟环境:尝试使用更真实的移动端UA,并保持会话( requests.Session)。 |
| 请求长时间无响应或返回非预期数据 | 1. 请求频率过高触发风控。 2. 接口地址或参数结构已变更。 | 1.降低频率:在请求间增加随机延时(如time.sleep(random.uniform(1, 3)))。2.验证接口:再次抓包,确认目标接口的URL和参数是否发生变化。 |
execjs调用报错,提示某些JS对象未定义 | JS代码中依赖了浏览器环境特有的对象(如window,document,navigator)。 | 补环境:在注入的JS代码开头,手动定义这些对象。例如:var window = this; var document = {}; var navigator = {userAgent: ‘…’};。更复杂的环境检测需要更细致的模拟。 |
| 算法看似正确,但成功率不高 | 1. 随机数r或指纹fp参与签名,且每次需不同。2. 存在“滑动验证”等二次验证,仅签名正确不足以通过。 | 1.动态参数:确保r(随机数)每次请求都重新生成。fp(指纹)可能需要一个稳定的生成算法,不能每次随机。2.应对验证码:如果遇到滑块或点选验证码,说明当前请求已被识别为高风险。需要降低频率、更换IP、或研究验证码破解(这属于另一个更复杂的领域)。 |
稳定性优化策略:
- Cookie池与更新机制:不要使用单一Cookie。构建一个Cookie池,并实现自动检测过期、自动刷新的逻辑(这可能需要模拟登录流程,难度较高)。
- IP代理池:对于大规模采集,使用高质量的住宅IP代理池是必须的,可以避免因单个IP请求过多被封。
- 请求参数动态化:除了
h5st中的t和r,其他如uuid、_t等参数也应尽量模拟真实客户端的生成规律。 - 错误重试与降级:网络请求加入重试机制(如
tenacity库)。对于非关键错误(如偶发的签名错误),可以记录日志并重试一次;对于明确的登录失效错误,则触发Cookie更新流程。 - 代码与算法同步:将核心的JS加密代码单独管理,并建立版本意识。一旦发现大量签名错误,第一时间去验证目标网站的JS代码是否更新。
逆向h5st这类参数是一个持续对抗的过程。没有一劳永逸的解决方案,核心能力在于快速定位问题、分析差异和更新代码的逻辑。这套从分析、逆向到实现的完整方法论,不仅能用于京东联盟,也能应用到其他具有类似签名反爬机制的平台上。关键在于保持耐心,细致比对,并善用工具。
