防重放中间件必须置于路由匹配后、Controller执行前,通过全局或路径级注册,用ctx.StopExecution()终止请求;需安全读取原始Body提取timestamp和nonce,校验时间窗口并用Redis原子操作去重。

防重放中间件必须放在路由匹配之后、controller执行之前
验签和防重放都属于请求预处理,失败必须立即中断,不能拖到业务逻辑里判断。否则可能已写库、发消息、扣库存——重放攻击就成功了。
注册方式只有两种可选:app.Use(replayMiddleware) 全局生效,或 app.Post("/api/pay", replayMiddleware, payHandler) 按路径启用。别用 app.Done(),它在 handler 执行完才调,完全没意义。
常见错误是把防重放逻辑塞进某个 payHandler 里:结果 /api/refund 绕过,攻击者反复刷退款接口。
- 中间件内必须用
ctx.StopExecution()终止链路,别只return;否则ctx.Next()还会继续执行 - 若需排除健康检查等免校验路径,得在中间件开头加判断:
if ctx.Path() == "/health" { ctx.Next(); return } - 别在
ctx.StopExecution()后再调ctx.Next(),Iris 会 panic
怎么安全读取原始请求体并提取时间戳和随机数
防重放依赖两个关键字段:客户端生成的 timestamp(秒级或毫秒级)和 nonce(一次性随机字符串),它们必须从原始 payload 解析,不能靠 ctx.FormValue() 或 ctx.URLParam() —— 因为前端可能走 JSON 提交,也可能走 form-data,字段位置不固定。
正确做法是先读原始 Body:body, _ := io.ReadAll(ctx.Request().Body),再用 ctx.Request().Body = io.NopCloser(bytes.NewReader(body)) 重置,保证后续 ctx.ReadJSON() 或 ctx.PostForm() 仍可用。
别用 ctx.GetBody():它返回的是 Iris 内部缓存副本,且可能已被 URL 解码或格式转换,nonce 里的特殊字符(如 +、/)会失真。
- JSON 场景:用
json.Unmarshal(body, &req)提取timestamp和nonce字段,结构体字段加json:"timestamp"显式映射 - form-data 场景:用
ctx.PostForm("timestamp")和ctx.PostForm("nonce"),但前提是 Content-Type 确实是multipart/form-data - 统一方案:强制所有接口约定用 JSON 提交,并在中间件里只处理
application/json,其他类型直接ctx.StatusCode(iris.StatusBadRequest)
时间戳校验和 nonce 去重怎么落地
时间戳校验不是简单比大小:要防止客户端时钟漂移,服务端应允许一定窗口(比如 ±300 秒),但窗口不能过大,否则重放窗口就变宽了。
nonce 去重不能只靠内存 map:单机部署可用 sync.Map 存 nonce → timestamp,但集群下必须用 Redis 的 SET key value EX 300 NX 原子操作,既去重又自动过期。
注意:Redis key 要包含时间戳前缀(如 nonce:1728421320:abc123),避免不同时间戳的相同 nonce 冲突;value 存时间戳,便于后续清理或审计。
- 校验顺序必须是:先检查
Content-Type→ 再读 body → 解析 timestamp/nonce → 校验 timestamp 是否在窗口内 → 最后查 nonce 是否已存在 - timestamp 窗口计算用
abs(now.Unix()-ts) > 300,别用now.Before(ts-300),Unix 时间戳是 int64,别误当 time.Time - Redis 连接必须带超时:
ctx.Timeout(2 * time.Second),防止单点故障拖垮整个请求链路
为什么不能把防重放逻辑写在 Controller 里
Controller 是 MVC 单例,所有请求共享同一实例。如果在 Controller 结构体里定义 seenNonces sync.Map,看似可行,但实际有严重隐患:sync.Map 是并发安全的,可它的 GC 不及时,大量过期 nonce 会长期驻留内存,OOM 风险高;更关键的是,你无法控制它的生命周期——重启服务后旧 nonce 不会自动清空,而新服务实例又没继承旧数据,导致部分重放请求漏判。
真正可靠的去重必须由外部存储承担(Redis / etcd),且校验动作必须前置到中间件,确保每个请求在进入业务前就被拦截。
另外,Iris 的 ctx.Params()、ctx.URLParam() 都无法拿到 body 里的字段,想在 Controller 里解析 timestamp 就得再读一次 Body —— 但 Body 已被中间件消费过,此时 ctx.Request().Body 是空的,除非你手动重置,这违背职责分离原则。


















