时间戳防重放必须结合redis等外部存储实现原子性去重,仅比对当前时间戳无效;需用set key value ex seconds nx确保同一时间戳首次出现,key应含用户id,窗口建议30–45秒,并将时间戳纳入签名防止篡改。

为什么时间戳防重放不能只比对 time.Now().Unix()
直接用当前时间戳和请求里的时间戳做简单相等或范围判断,根本拦不住重放——攻击者只要在有效窗口内重发合法请求就行。关键不是“时间对不对”,而是“这个时间戳是不是第一次出现、且没过期”。必须配合服务端状态存储(比如 Redis)或分布式唯一性校验,否则纯内存 map 在多实例下完全失效。
- 单机用
sync.Map只能防本进程重放,跨 goroutine 也不安全,更别说多节点 - 时间窗口建议控制在
30s内,太长增加风险,太短影响时钟漂移大的客户端 - 注意服务器和客户端时钟偏差:别依赖绝对时间一致,用「接收时间 - 请求时间」算偏移,再结合窗口判断
如何用 Redis 实现原子性时间戳去重
核心是 SET key value EX seconds NX:利用 Redis 的原子性写入,确保同一个时间戳(拼上用户 ID 或请求 ID 更稳妥)最多成功一次。失败即判定为重放。
- key 建议设计为
"replay:uid123:<timestamp>"</timestamp>,避免不同用户间误冲突 - value 可填随机字符串或客户端传的
nonce,便于后续审计 - EX 时间应略大于业务允许的最大时钟偏差 + 网络延迟,比如
45秒 - Go 客户端推荐用
redis.UniversalClient,调用Set(ctx, key, value, ttl).Err()判断是否返回redis.Nil(表示 set 成功)
crypto/hmac 和时间戳组合能防篡改,但不解决重放
很多人把签名和防重放混为一谈。加了 HMAC 确保时间戳没被改,但攻击者照搬整个带签名的请求照样重放。必须把防重放逻辑放在签名验证之后、业务处理之前。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 典型顺序:
解析参数 → 校验签名 → 检查时间戳是否在窗口内 → 查询 Redis 去重 → 执行业务 - 时间戳字段必须参与签名计算,否则攻击者可随意改时间绕过窗口检查
- 别用
time.Now().UnixMilli()当作唯一标识——毫秒级在高并发下极易碰撞,至少拼上随机nonce或序列号
容易被忽略的边界:NTP 同步、Docker 容器时钟、云函数冷启动
线上环境的真实时钟行为比本地开发复杂得多。K8s Pod 重启、Lambda 冷启动后首次调用,time.Now() 可能跳变几十毫秒;某些容器镜像甚至禁用 NTP。
- 不要假设
time.Since()总是单调递增,尤其在虚拟化环境中 - 生产环境务必开启 NTP 服务,并监控时钟偏移(如用
chrony tracking) - 对延迟敏感的服务,考虑用
time.Now().UnixNano()+runtime.GC()前后采样做粗略 jitter 检测(非常规手段,仅作兜底) - 测试阶段用
ginkgo或testify模拟时钟漂移,比如强制设置系统时间为 ±5s 后发请求
实际部署时,Redis 连接池超时、网络分区、key 过期策略这些细节,比算法本身更容易导致漏防。别在防重放逻辑里加日志阻塞主流程,也别让去重检查耗时超过 5ms——它得扛住每秒几千次请求。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










