必须用 gin 中间件提取 x-nonce 和毫秒级 x-timestamp,校验时间差≤300000ms,再通过 redis set key value ex 300 nx 原子存 nonce;仅对非幂等方法启用,get 跳过,redis 失败时降级为仅校验 timestamp。

怎么用 Gin 中间件校验 timestamp 和 nonce
防重放不是靠“拦截重复请求”,而是靠“拒绝过期或已用过的请求”。Gin 本身不提供时间/nonce 校验能力,必须自己写中间件,核心是提取请求头或参数、做时间差判断、再用 Redis 原子命令存 nonce。
常见错误是直接读 c.Request.Body 后没重置,导致后续 c.ShouldBindJSON 失败;或者用 GET 参数传 timestamp 却没校验 method 类型,被绕过。
- 只对
POST、PUT、DELETE等非幂等方法启用校验,GET查询类接口跳过 - timestamp 必须是毫秒级整数,且与服务端时间差 ≤ 5 分钟(建议设为 300000ms),超过直接
c.AbortWithStatus(403) - nonce 推荐从
X-Nonce请求头取,而非 query 或 body——避免被缓存或日志泄露 - Redis 写入用
SET key value EX 300 NX,返回nil表示已存在,此时拒绝请求
签名验证要不要加?加在哪一步
单纯 timestamp + nonce 可防简单重放,但挡不住篡改。签名是必要增强项,它把 timestamp、nonce、path、body、method 全部参与计算,让攻击者无法只改时间戳或换 nonce。
容易踩的坑是:body 读取两次(一次算签名、一次 bind),没用 ioutil.ReadAll + c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes)) 重置;或签名字符串拼接顺序不一致(比如前端按字母序排参数,后端按字典序排,结果不匹配)。
- 签名密钥不硬编码,从配置或 Vault 加载,不同 client 分配不同
accessKey/secretKey - 签名字段统一放在
X-Signature请求头,避免混在 body 里增加解析负担 - 签名前先 normalize body:去掉空格、统一换行符、JSON 字段排序(可用
jsoniter.ConfigCompatibleWithStdlib.SortMapKeys(true)) - 校验失败统一返回
401 Unauthorized,不暴露是 timestamp 错还是签名错
Redis 连接失败时怎么降级
生产环境 Redis 挂了不能直接让所有写接口 500,得有兜底策略。最稳妥的是“告警+放行”,而不是“阻断+报错”。
很多人忽略连接池超时和重试逻辑,一连不上就 panic,反而放大故障面。Gin 中间件里不该用 redis.Dial 这种原始方式,而要用 redis.NewClient 并设置 WithContext 超时。
- 连接失败或
SET超时(比如 >200ms),记录 warn 日志并return,不调c.Abort() - 用
atomic.Bool控制降级开关,当连续 5 次 Redis 失败,自动开启“只校验 timestamp”模式,持续 60 秒 - 降级期间在响应头加
X-Auth-Mode: degraded,方便链路追踪识别 - 绝对不要 fallback 到本地 map 或 sync.Map —— 集群下无效,且内存泄漏风险高
为什么不能只依赖前端生成的 nonce
前端生成的 UUID 或随机字符串看似唯一,但在真实场景中极易复用:WebView 缓存、PWA 离线重发、iOS Safari 的 background fetch 都可能导致同一 nonce 提交多次。
更严重的是,攻击者可截获一次合法请求,提取出 nonce + timestamp,稍作延时再重放——只要没签名绑定,服务端无法区分真假。
- nonce 必须和服务端时间戳强绑定,比如组合成
sha256(nonce + timestamp + secret)再存 Redis - 若业务允许,推荐服务端生成 nonce 并通过
Set-Cookie或响应头下发,客户端下次请求必须携带 - 对登录、支付等关键接口,额外要求用户设备指纹(如 UA + IP + 屏幕分辨率哈希)参与校验,提高重放成本
- Redis key 命名别只用
nonce:,建议带业务前缀和租户 ID,例如nonce:pay:tenant_abc:uuid123











