echo的echo.middlewarefunc不能直接防重放,因其仅处理已进入路由链的合法请求,而重放攻击需在中间件中强制实施时间戳、nonce与签名三元原子校验,且必须用redis set nx ex确保nonce唯一性与自动过期。

为什么 Echo 的 echo.MiddlewareFunc 不能直接防重放?
因为重放攻击发生在应用层协议之外——它不关心你用的是 Echo、Gin 还是原生 net/http。Echo 的中间件只处理已进入路由链的请求,而重放请求在语义上完全合法:URL、Header、Body 都能通过基础校验。真正要拦住它,必须在中间件里做「时间戳 + nonce + 签名」三元校验,且校验逻辑必须原子化、不可绕过。
如何在 Echo 中实现带时间窗口的请求签名校验?
核心是拦截所有敏感接口(如 /api/v1/login),提取 timestamp、nonce 和 signature 三个 Header 或 Query 参数,再比对:
-
timestamp是否落在服务端允许的时间窗口内(建议 ≤ 300 秒,避免时钟漂移) -
nonce是否已在 Redis 中存在(用SET key "" EX 300 NX原子写入,失败即拒绝) -
signature是否由HMAC-SHA256(secret, timestamp + ":" + nonce + ":" + body)生成(注意 body 必须是原始字节流,不能经echo.Context.Request().Body多次读取)
示例关键片段:
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
func ReplayProtection() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ts := c.Request().Header.Get("X-Timestamp")
nonce := c.Request().Header.Get("X-Nonce")
sig := c.Request().Header.Get("X-Signature")
if ts == "" || nonce == "" || sig == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "missing required headers")
}
now := time.Now().Unix()
t, _ := strconv.ParseInt(ts, 10, 64)
if now-t > 300 || t-now > 300 {
return echo.NewHTTPError(http.StatusUnauthorized, "timestamp expired")
}
key := "replay:" + nonce
ok, err := rdb.SetNX(c.Request().Context(), key, "", 300*time.Second).Result()
if err != nil || !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "nonce reused")
}
body, _ := io.ReadAll(c.Request().Body)
c.Request().Body = io.NopCloser(bytes.NewReader(body))
expected := hmacSign(string(body), ts, nonce)
if !hmac.Equal([]byte(sig), []byte(expected)) {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid signature")
}
return next(c)
}
}
}
为什么不能把 body 读两次?
Go 的 http.Request.Body 是单次可读的 io.ReadCloser,Echo 默认不会缓存它。如果你在中间件里调用 io.ReadAll() 一次,后续 handler 再读就会得到空内容,导致业务逻辑解析失败(比如 c.Bind() 报 EOF)。必须手动重建 Body:
- 用
bytes.NewReader(body)创建新 reader - 包一层
io.NopCloser()满足接口要求 - 赋值回
c.Request().Body
漏掉这一步,签名能过,但下游业务拿不到数据 —— 这是最常被忽略的断点。
Redis 存 nonce 为什么必须设 TTL?
nonce 本身无业务含义,只用于“一次性”标识。不设过期时间会导致 Redis 键无限堆积,最终 OOM。TTL 必须 ≥ 时间窗口(如 300 秒),否则可能出现「时间未超但 nonce 已被删」的误拒。另外,SET ... NX EX 要求 Redis ≥ 2.6.12,低版本需拆成 SETNX + EXPIRE 两步(非原子,有竞态风险)。










