JWT本身无法防重放,因其标准解析仅校验签名、exp和iat,不检查请求是否已处理;必须在JWT解析后、业务执行前,用Redis校验“用户+秒级时间戳+nonce”构成的唯一指纹,且超时设为30–60秒。

防重放不能只靠 JWT 解析,必须在中间件里用 Redis 做请求指纹校验,否则任何“时间戳 + 签名”组合都可被截获重放。
为什么 jwt.Parse 之后还必须加一层校验
标准 jwt.Parse 只检查签名、exp 和 iat,不关心这个请求是否已处理过。攻击者拿到一次合法请求(比如支付回调),只要在 Token 有效期内重发,服务端照样解析成功——因为 JWT 是无状态的,它不也不该承担去重职责。
- 强行把
nonce塞进 JWT payload 并全局查重,等于放弃无状态优势,还要额外维护 Redis 清理逻辑 - 防重放的本质是确保「同一用户 + 同一时间窗口 + 同一随机因子」的组合只能命中一次,不是验证 Token 是否合法
- 校验必须发生在 JWT 解析成功之后、业务逻辑执行之前,且要复用已解析出的
userID
Redis 指纹 key 怎么构造才安全又高效
指纹不是存整个 Token,而是绑定用户粒度 + 秒级时间窗口 + 客户端随机数,避免毫秒精度带来的 Redis 压力:
- key 格式必须为
req:{userID}:{timestamp}:{nonce},其中timestamp用time.Now().Unix()(秒级),不是毫秒 - 必须用
rdb.SetNX(ctx, key, "1", 60*time.Second),不能先GET再SET,否则并发下会漏判 - 超时时间建议 30–60 秒:太短易因网络延迟误拒,太长则防重放窗口变大
- 如果客户端无法传
X-Nonce,可用sha256.Sum256(body)[:8]替代,但 GET 请求无 body,得统一 fallback 规则(比如用 path + query string)
客户端 Header 必须强制约定哪两个字段
服务端再严,客户端乱填就失效。必须校验以下两个 header 的存在性、格式和合理性,缺一即返回 400 Bad Request,不进入 JWT 解析流程:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
-
X-Timestamp:必须是 Unix 秒级时间戳,且与服务端时间偏差 ≤ 30 秒(防止时钟漂移) -
X-Nonce:长度 ≥ 16 字符,只含字母数字,前端用crypto/rand生成,不能重复 - 两者必须走 header,不能从 URL 参数或 form 字段读取——避免被日志、代理、CDN 意外泄露
中间件嵌入位置和复用要点
防重放中间件必须复用 JWT 中间件已解析出的 userID,不能自己再 parse 一遍 Token:
- JWT 中间件解析成功后,把
userID注入context.Context,例如:ctx = context.WithValue(ctx, "user_id", claims.UserID) - 防重放中间件从
context取userID,再读X-Timestamp和X-Nonce,拼 fingerprint、查 Redis - 失败直接
http.Error(w, "request replay detected", http.StatusForbidden),不调用next.ServeHTTP - 别在 handler 里写校验逻辑——分散校验容易遗漏,也违背单一职责
真正难的不是代码,是前后端对时间同步、nonce 生命周期、错误码语义的一致理解;一个环节松动,整套机制就形同虚设。

















