h5st 的特殊之处:算法是动态下发的
京东 H5 接口的签名参数 h5st 有一个和其他家都不同的设计:签名算法本身不是静态写死在 JS 里的,而是 SDK 启动时从服务器拉取的。这就是为什么你在 h5st.js 里翻来覆去找不到完整的签名逻辑——核心部分是个"算法函数",运行时从 https://cactus.jd.com/request_algo?g_ty=ajax 拉下来 eval 执行。
理解这一点,逆向思路就清晰了:不是去逆一个静态算法,而是搞清楚"拉算法 → 组装输入 → 执行算法"这三段流程。
h5st 参数结构
先看参数长什么样(以 4.x 版本为例,分号分段):
h5st = 日期时间;设备指纹fp;appid;token;时间戳;版本号;随机串;签名结果各段含义:
- 第 1 段:
yyyyMMddHHmmssSSS格式的本地时间 - 第 2 段:fp,设备指纹,由独立指纹 SDK 生成
- 第 3 段:appid,接口所属应用标识(每个业务线不同,在接口文档或抓包里能看到)
- 第 4 段:token,request_algo 接口下发的算法令牌
- 第 5 段:毫秒时间戳
- 第 6 段:算法版本,如
4.9 - 第 7 段:随机串
- 第 8 段:签名结果,HMAC-SHA256 的 hex
签名流程三段论
第一段:拉算法。SDK 初始化时 POST request_algo,带上 fp、appid、版本号、时间戳和一个随机串,响应返回:
{
"data": {
"result": {
"algo": "function(token, fingerprint, timestamp, appId, ...) {...}",
"tk": "tk04w...", // 算法令牌
"timestamp": "..."
}
}
}algo 字段就是签名函数的源码字符串,eval 之后得到签名函数。tk 是参与签名的令牌,有有效期。
第二段:组装待签串。签名函数的输入是一个固定模板的拼接串,包含:
appid + functionId + body的SHA256 + client + clientVersion + t(秒级时间戳) + ...注意 body 参与的是 SHA256 摘要而不是原文,且 body 必须是 JSON.stringify 的紧凑格式(无空格)。模板各字段的顺序和分隔符随版本不同,这就是为什么必须抠当前版本的 algo 函数。
第三段:执行签名。algo 函数内部对待签串先做 SHA-256,再用 tk 派生的密钥做 HMAC-SHA256,输出 hex 作为第 8 段。
工程实现
import hashlib, hmac, json, time, requests
class H5stSigner:
def __init__(self, appid, fp):
self.appid = appid
self.fp = fp
self.tk = None
self.algo = None # eval 出来的算法,或等价 Python 实现
def refresh_algo(self, session):
ts = str(int(time.time() * 1000))
resp = session.post('https://cactus.jd.com/request_algo?g_ty=ajax', json={
"version": "4.9", "fp": self.fp, "appId": self.appid,
"timestamp": ts, "platform": "web", "expandParams": ""
}, headers={"Content-Type": "application/json"}).json()
self.tk = resp['data']['result']['tk']
# algo 字符串保存下来,用 Node 执行或人工翻译成 Python
def sign(self, function_id, body_obj, client='wh5', client_ver='1.0.0'):
t = str(int(time.time()))
body = json.dumps(body_obj, separators=(',', ':'))
body_hash = hashlib.sha256(body.encode()).hexdigest()
raw = f"{self.appid}{function_id}{body_hash}{client}{client_ver}{t}" # 模板以实际 algo 为准
digest = hashlib.sha256(raw.encode()).hexdigest()
sign = hmac.new(self.tk.encode(), digest.encode(), hashlib.sha256).hexdigest()
return sign上面的模板是示意结构——正确做法是把 algo 函数抠出来在 Node 里跑,因为它除了拼接还有细节处理(比如随机串参与方式、二次摘要),手翻容易丢步骤。
精髓:四个实战要点
- fp 指纹是独立 SDK 的产物,采集 canvas、ua、屏幕等。协议模拟时从真实浏览器采一个固定使用,和 ua 成套。fp 异常会直接返回"参数错误"而不是签名错误,很有迷惑性。
- tk 有有效期且和 fp 绑定。刷新 algo 时换 fp 必须重新拉 tk,旧 tk 配新 fp 签名必错。
- 版本碎片化严重。京东各业务线 h5st 版本不同(3.x/4.x 并存),分段数量、模板、密钥派生方式都有差异。封装时把"分段模板"做成配置,接新业务线先抓包对照。
- 错误无反馈。签名错了接口经常返回空数据或通用错误码,不报"签名错误"。调试时用已知的简单接口(如商品详情)做签名正确性基准,别在复杂业务接口上空耗。
总结
- h5st 的核心设计是"算法动态下发",request_algo 是突破口
- 签名输入含 body 的 SHA256,body 必须紧凑 JSON
- fp、tk、appid 三者绑定,成套管理
- 优先抠 algo 函数到 Node 执行,手翻模板容易丢细节