百趣云 百趣云的博客

京东 h5st 签名算法逆向实战

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 里跑,因为它除了拼接还有细节处理(比如随机串参与方式、二次摘要),手翻容易丢步骤。

精髓:四个实战要点

  1. fp 指纹是独立 SDK 的产物,采集 canvas、ua、屏幕等。协议模拟时从真实浏览器采一个固定使用,和 ua 成套。fp 异常会直接返回"参数错误"而不是签名错误,很有迷惑性。
  2. tk 有有效期且和 fp 绑定。刷新 algo 时换 fp 必须重新拉 tk,旧 tk 配新 fp 签名必错。
  3. 版本碎片化严重。京东各业务线 h5st 版本不同(3.x/4.x 并存),分段数量、模板、密钥派生方式都有差异。封装时把"分段模板"做成配置,接新业务线先抓包对照。
  4. 错误无反馈。签名错了接口经常返回空数据或通用错误码,不报"签名错误"。调试时用已知的简单接口(如商品详情)做签名正确性基准,别在复杂业务接口上空耗。

总结

  1. h5st 的核心设计是"算法动态下发",request_algo 是突破口
  2. 签名输入含 body 的 SHA256,body 必须紧凑 JSON
  3. fp、tk、appid 三者绑定,成套管理
  4. 优先抠 algo 函数到 Node 执行,手翻模板容易丢细节
By 百趣云 阅读量:2 On