防重放攻击必须同时校验timestamp与nonce:timestamp限定请求有效期(如[now−300, now+10]),nonce通过redis原子操作(setex或set nx ex)确保单次使用,二者须共同参与签名计算且不可缺一。

防重放攻击不是加个时间戳就完事——关键在于服务端必须严格校验 timestamp 与 nonce 的组合有效性,且两者缺一不可。
为什么单纯校验 timestamp 不够安全
只比对请求中的 timestamp 是否在 5 分钟内,会漏掉同一时间戳下重复提交的请求。攻击者完全可以截获一次合法请求,毫秒级重放多次,只要时间仍在窗口内,服务端就会放行。
- 时间戳(
timestamp)用于限定请求有效期,防止“过期但未被用过的请求”被重放 - 随机数(
nonce)用于标识单次请求唯一性,必须服务端存储并去重(如 Redis 中缓存nonce+timestamp组合,TTL 设为略大于时间窗口,例如 310 秒) - 两者必须同时参与签名计算,否则客户端可篡改其一绕过校验
gin 中间件里怎么校验 timestamp 和 nonce
在 Gin 中间件中解析请求头或 query 参数时,需统一提取 timestamp、nonce 和 signature,再执行三步校验:
- 检查
timestamp是否为整数秒格式,且与服务器当前时间差 ≤ 300 秒(建议用time.Now().Unix()对比,避免时区误差) - 查询 Redis 中是否存在该
nonce;若存在,直接拒绝(已用过);若不存在,立即写入nonce并设 TTL = 310 秒 - 重新拼接签名原文(含 method、path、sorted query、body hash、
timestamp、nonce),用相同secretKey计算 HMAC-SHA256,用hmac.Equal()比对,禁止直接用 ==
示例关键判断逻辑:
if ts, err := strconv.ParseInt(c.GetHeader("X-Timestamp"), 10, 64); err != nil || time.Now().Unix()-ts > 300 || ts-time.Now().Unix() > 60 {<br> c.AbortWithStatusJSON(401, gin.H{"error": "invalid timestamp"})<br> return<br>}
nonce 存哪里?Redis 还是内存 map
生产环境必须用 Redis(或类似分布式缓存),不能用本地 map —— 多实例部署下,内存 map 完全无法共享状态,nonce 去重失效,等于没防。
- key 设计建议:
nonce:{app_id}:{nonce},便于按应用隔离 - value 可为空,仅依赖 key 存在性判断;或存
timestamp值用于调试追踪 - 务必设置 TTL,且 TTL > 时间窗口(如 300 秒窗口 → TTL 设 310 秒),避免因网络延迟导致合法请求被误拒
- Redis 写失败(如超时)应拒绝请求,不降级为跳过 nonce 校验
签名原文拼接顺序容易出错
客户端和服务端对参数排序规则不一致,会导致签名永远不匹配。Gin 默认不自动排序 query 参数,必须显式处理。
- 不要用
c.Request.URL.Query()直接遍历,它返回的 key 顺序不确定 - 正确做法:用
c.Request.URL.RawQuery解析后,取所有 key 放入 slice,sort.Strings(keys),再按序拼key=value - Body 必须统一哈希:GET 请求 body 为空,POST/PUT 等需先读取
c.Request.Body,计算sha256.Sum256,取 hex 字符串参与拼接;注意读完要io.ReadCloser重置,否则后续绑定失败 - 路径必须用
c.Request.URL.Path(不含 query),不要用c.FullPath()(含路由变量,不稳定)
拼接模板示例:POST&/api/v1/order&amount=100×tamp=1723488540&nonce=abc123&body_hash=e3b0c44298fc1c149afbf4c8996fb...
真正难的不是写对一次签名,而是让客户端和服务端在任意参数组合、任意 HTTP 方法、任意 body 类型下,始终生成完全一致的签名原文——这要求双方严格约定并测试所有边界 case,比如空值、URL 编码字符、重复 key 等。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











