jwt本身无法防重放,因其标准解析仅校验签名、exp和iat,不检查请求是否已处理;攻击者可在有效期内重放合法请求,而jwt默认无状态,不应承担去重职责。

为什么 JWT 本身无法防重放
标准 jwt.Parse() 只校验签名、exp 和 iat,不记录请求是否已处理。攻击者截获一次合法请求(比如转账),只要在 Token 有效期内重放,服务端照样解析通过——因为 JWT 是无状态的,它不也不该承担“去重”职责。
强行把 nonce 塞进 JWT payload 并全局查重,等于放弃无状态优势,还要额外维护 Redis 清理逻辑,得不偿失。
用 Redis 实现轻量级请求指纹校验
核心是构造唯一、短时效、可快速判定的指纹,不是存 Token,而是绑定用户 + 时间窗口 + 随机因子:
-
fingerprint := fmt.Sprintf("req:%s:%d:%s", userID, timestamp, nonce)——userID来自 JWT claims,timestamp用秒级(避免毫秒带来 Redis 压力),nonce由客户端生成并传入 - 必须用
rdb.SetNX(ctx, fingerprint, "1", 60*time.Second),不能先GET再SET,否则并发下会漏判 - 超时时间建议 30–60 秒:太短易因网络延迟误拒,太长则防重放窗口变大
- 若客户端无法传
nonce,可用sha256.Sum256(body)[:8]替代,但需注意 GET 请求无 body,得统一约定 fallback 规则
客户端必须配合的 Header 约定
服务端再严,客户端乱填就失效。必须强制校验两个 header 的存在性、格式和合理性:
-
X-Timestamp:必须是 Unix 秒级时间戳,且与服务端时间偏差 ≤ 30 秒(防止时钟漂移) -
X-Nonce:长度 ≥ 16 字符,只含字母数字,不能重复(前端可用crypto/rand生成) - 两者缺一即返回
400 Bad Request,不进入 JWT 解析流程 - 不要接受 URL 参数或 form 字段传这些值,必须走 header,避免被日志/代理意外泄露
中间件里怎么嵌入校验逻辑
防重放必须在 JWT 解析之后、业务处理之前做,且要复用已解析出的 userID:
- JWT 中间件解析成功后,把
userID注入context.Context,供后续中间件取用 - 防重放中间件从 context 取
userID,再读X-Timestamp和X-Nonce,拼指纹、查 Redis - 失败直接
http.Error(w, "request replay detected", http.StatusForbidden),不调用next.ServeHTTP - 别在 handler 里写校验逻辑——分散校验容易遗漏,也违背单一职责
真正难的不是代码,是前后端对时间同步、nonce 生命周期、错误码语义的一致理解;一个环节松动,整套机制就形同虚设。











