从一个 sign 参数入手:还原某电商平台商品详情接口的签名算法
商品详情接口带一个 32 位的 sign,改动任意查询参数都会返回 403。整个过程分三步:定位生成位置、剥离混淆、脱离浏览器环境复现。
商品详情接口带一个 32 位的 sign,改动任意查询参数都会返回 403。这类参数是采集里最常见的第一道门槛,处理思路基本固定:先确认它由前端生成而不是服务端下发,再定位生成位置,最后脱离浏览器环境复现。
请求长什么样#
抓包结果里除业务参数外多了 t、nonce、sign 三个字段。t 是毫秒时间戳,nonce 每次刷新都变,sign 长度固定 32 位,大概率是 MD5,但盐值与拼接顺序需要确认。
判断是不是前端生成,最快的办法是把 t 往前调几分钟再发一次。如果服务端只校验签名一致性而不校验时间窗,说明整套逻辑都在客户端;如果返回时间戳过期,那至少时间窗是服务端在管。
GET /api/v2/item?id=1024
&t=1755772800123
&nonce=8f2c1a
&sign=c41d8f...9e2b{
"code": 40301,
"msg": "invalid signature"
}定位生成位置#
直接在 Sources 里搜 sign 会有数十处命中,大多是无关的业务字段。更快的路径是从加密库反向找:这类站点绝大多数用 CryptoJS,搜 CryptoJS.MD5 通常只有个位数结果。
打上 XHR 断点后向回翻三层堆栈,落点在打包后的请求拦截器。整条链路如下,方括号内为压缩后的函数名。
- 01
r.request()业务层发起请求 - 02
e.interceptors统一拦截器,注入公共参数 - 03
n(t, o)按字典序拼接 key=value - 04
a(raw + SALT)追加硬编码盐值 - 05
CryptoJS.MD5取 32 位小写摘要 - →
sign写回 query,请求放行
如果混淆程度高到堆栈不可读,可以直接改写方法本身,让它在被调用时把入参打出来。这一步不需要理解任何混淆代码:
// 拦截所有走这个方法的调用,命中即断下
const _md5 = window.CryptoJS?.MD5
window.CryptoJS.MD5 = function (msg) {
console.log('[md5 in]', msg.toString())
debugger
return _md5.apply(this, arguments)
}命中后控制台打印的明文串即为拼接结果,顺序一目了然。这个技巧对 JSON.stringify、btoa、encodeURIComponent 同样适用,是定位阶段性价比最高的一招。
脱环境复现#
明文格式确认为 key=value 按字典序拼接,尾部追加一段硬编码盐。剩下的部分用 Python 几行即可复刻,不需要挂 Node 环境。
import hashlib
import random
import time
SALT = "a3f9...c7"
def make_sign(params: dict) -> str:
params["t"] = int(time.time() * 1000)
params["nonce"] = f"{random.getrandbits(24):06x}"
raw = "&".join(f"{k}={params[k]}" for k in sorted(params))
return hashlib.md5((raw + SALT).encode()).hexdigest()验证方式是拿抓包里的原始参数(包括原始的 t 和 nonce)跑一遍,看输出是否和抓到的 sign 完全一致。对不上就说明拼接细节还有偏差,常见的坑有三个:值需要 URL 编码后再拼、空值参数要不要参与、盐是前缀而不是后缀。
小结#
这类固定算法的签名,难点从来不在算法本身——MD5 拼盐是最常见的一种——而在定位和验证。定位靠 hook 而不是读代码,验证靠原始参数逐位对齐而不是直接发请求试。这两条守住,大部分同类参数都能在一两个小时内还原完。