🤔 为什么攻击者修改随机数后依然无法通过?
假设攻击者截获了一个合法的请求:
http
POST /api/transfer
X-Timestamp: 1722240000
X-Nonce: abc123
Authorization: Bearer <合法的JWT>
现在,攻击者想重放这个请求,他尝试修改随机数:
http
POST /api/transfer
X-Timestamp: 1722240000
X-Nonce: xyz789 <-- 修改了随机数
Authorization: Bearer <合法的JWT>
这时候,服务端会进行两步校验:
- 验证 JWT 签名:攻击者手里有原始的 JWT,这个 JWT 本身是合法的(没过期,签名正确)。所以这一步能通过。
- 验证请求指纹 :服务端用
用户ID + 时间戳 + 新随机数生成指纹xyz789,去 Redis 里查,发现没记录过,于是放行。
看起来攻击成功了?并没有!
因为真正的防重放机制,并不是只校验 JWT 和随机数,而是将请求的关键部分(如时间戳、随机数、甚至请求体)与 JWT 绑定在一起进行签名。
🔒 真正的防重放:请求签名
为了防止攻击者篡改请求的任何部分(包括随机数),我们需要对整个请求进行签名。
正确的流程是这样的:
-
客户端准备数据:
- 生成时间戳
X-Timestamp - 生成随机数
X-Nonce - 准备请求体
Body
- 生成时间戳
-
客户端生成签名:
- 将
X-Timestamp + X-Nonce + Body拼接起来。 - 使用一个只有客户端和服务端知道的密钥 (可以是 JWT 的密钥,也可以是单独的 AppSecret)对这个拼接字符串进行 HMAC-SHA256 等哈希运算,生成一个签名
Signature。 - 将
Signature放入请求头(例如X-Signature)。
- 将
-
客户端发送请求:
httpPOST /api/transfer X-Timestamp: 1722240000 X-Nonce: abc123 X-Signature: <计算出的签名> Authorization: Bearer <合法的JWT> Body: {"amount": 100, "to": "userB"} -
服务端校验:
- 第一步:验证 JWT,确认用户身份。
- 第二步:验证签名 。服务端用同样的密钥,对收到的
X-Timestamp + X-Nonce + Body重新计算一遍签名。 - 第三步:比对签名 。如果服务端计算出的签名和请求头里的
X-Signature一致,说明请求内容(包括时间戳、随机数、请求体)在传输过程中没有被篡改。 - 第四步:防重放检查 。将
X-Timestamp + X-Nonce存入 Redis,检查是否重复。
💡 思考
如果攻击者现在尝试修改随机数:
http
POST /api/transfer
X-Timestamp: 1722240000
X-Nonce: xyz789 <-- 修改了随机数
X-Signature: <原始的签名> <-- 签名没变
Authorization: Bearer <合法的JWT>
Body: {"amount": 100, "to": "userB"}
服务端在校验时,会用新的 X-Nonce 重新计算签名,结果会发现计算出的签名和请求头里的 X-Signature 不一致,于是直接拒绝请求。
总结一下:
- 只靠 JWT + 随机数:确实防不住篡改随机数的重放。
- JWT + 随机数 + 请求签名:这才是完整的防重放方案。签名确保了请求的完整性,任何对请求内容的修改(包括随机数)都会导致签名校验失败。