签名与防重放:时间戳 / nonce / 盐
本节目标
- 理解「签名」如何防篡改、「防重放」如何防复用
- 用 Node 跑通「时间戳 + nonce + 盐」的签名示例
- 知道逆向时这三样东西分别去哪找
签名防「请求被改」,防重放防「截获的请求被反复发」。常见组合:把「参数 + 时间戳 + 随机 nonce + 盐(密钥)」一起算签名。
// 运行环境:Node.js 18+(无需依赖,用内置 crypto)
// 运行:node jsr-l10.cjs
const crypto = require('crypto')
function makeRequestSign(params, salt) {
const ts = Date.now() // 时间戳:证明请求是「现在」的
const nonce = Math.random().toString(36).slice(2) // 随机数:每次不同
const raw = [ts, nonce, JSON.stringify(params), salt].join('|')
const sign = crypto.createHash('md5').update(raw).digest('hex')
return { ts, nonce, sign } // 这三个都随请求发出
}
const salt = 'SERVER_SALT' // 盐:只有前后端知道的密钥
const req = makeRequestSign({ action: 'list' }, salt)
console.log(req)
// 服务端用同样的 ts/nonce/params/salt 重算 sign,对得上才处理;
// 且 ts 和上次太近/nonce 重复则拒绝(防重放)逆向要点:
salt(盐/密钥):最难找,可能在 JS 里硬编码、也可能从接口下发。ts:当前时间戳,复现时Date.now()即可,但要注意单位(秒/毫秒)。nonce:随机串,复现也要随机生成(或按它算法生成)。
名词解释
防重放(Replay Attack Protection):防止「攻击者截获一个合法请求,原样重发无数次」的防护。做法通常是要求请求带「时间戳(过期作废)+ 唯一 nonce(用过即废)」,服务端记下来,重复或超时的直接拒。逆向时你要复现的不仅是 sign,还有「合法的 ts 和没用过的 nonce」。
盐(Salt):加在签名/哈希原料里的「只有双方知道的秘密串」。它的作用是让同样的明文算出不同的签名(防彩虹表/防猜),也把「密钥」和「数据」分开。逆向里 salt 往往藏在 JS 某个常量或闭包里,是签名还原的「最后一块拼图」。
课后练习
练习 1:如果逆向时 salt 找不到,但 sign 算法其他都确认了,会怎样?
答案:算出的 sign 永远和网站不一致,请求被拒。salt 是签名还原的「最后一公里」,找不到就差这一步。常见藏身处:JS 顶层常量、webpack 模块导出、或某个接口返回的
appKey。要多处搜关键字(salt/secret/key/appKey)。
练习 2:ts 单位用错(秒 vs 毫秒)会有什么后果?
答案:服务端校验
now - ts < 60秒时,如果你传的是秒级时间戳(如 1700000000)而它按毫秒比较(期望 1700000000000),差值巨大 → 判定「过期」直接拒绝。复现务必确认 ts 单位,常见坑。
总结
签名与防重放是「前端逆向最后一公里」最常卡住的地方,而卡点几乎永远是 salt(盐)。我的观点:很多人算法读得明明白白,却栽在「找不到那把盐」上。所以定位阶段就要有意识地去搜 salt/secret/appKey/key 这些关键字,别等最后才找。另外注意两个隐形坑:时间戳单位(秒还是毫秒)和 nonce 是否要按它的算法生成(有些站 nonce 不是纯随机,而是有规律的)。把「ts / nonce / salt / 拼接顺序」列成 checklist,逐项对齐,签名这关就能过。防重放机制本身也提醒你:复现时 nonce 要用新的,别傻傻复用抓包里的旧值。