直接用 time.Now().Unix() 校验 Cookie 时间戳会失效,因时钟不同步且 Cookie 未签名可被篡改;必须结合 HMAC 签名与 MaxAge 过期双保险,并在中间件中严格校验时间有效性、签名正确性及滑动窗口。

为什么直接用 time.Now().Unix() 做 Cookie 时间戳校验会失效
因为服务端和客户端时钟不同步,哪怕差几秒,重放请求就可能绕过校验。更关键的是,echo.Context.SetCookie() 默认不加密、不签名,攻击者可以篡改 Cookie 里的时间戳字段(比如 ts=1715823400)并重发——你校验的其实是被伪造的时间。
所以核心不是“怎么取时间”,而是“怎么让时间戳不可篡改 + 有实效性”。
必须用 echo.Cookie 配合签名 + 过期时间双保险
Echo 自带的 echo.Cookie 支持 Secure、HTTPOnly 和 SameSite,但签名得自己加。推荐在写入 Cookie 前,把时间戳和一个服务端密钥一起 HMAC 签名,再拼进值里:
ts := time.Now().Unix()
sig := hmac.Sum256([]byte(fmt.Sprintf("%d:%s", ts, secretKey))).String()[:16]
cookieValue := fmt.Sprintf("%d:%s", ts, sig)
<p>c.SetCookie(&http.Cookie{
Name: "auth_token",
Value: cookieValue,
MaxAge: 300, // 5分钟,强制客户端过期
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})注意:MaxAge 是客户端强制丢弃时间,和后端校验逻辑是两回事,但必须设——否则攻击者可无限次重放旧 Cookie。
读取时校验三件事:ts 是否过期、sig 是否匹配、ts 是否在合理滑动窗口内
不能只比对当前时间减去 ts 是否超 5 分钟,要留一点容忍(比如 30 秒),否则网络延迟或轻微时钟偏移会导致合法请求失败。
- 从
c.Cookie("auth_token")解析出ts和sig,格式为"1715823400:abcd1234..." - 检查
ts是否为整数且非负,防止注入或溢出 - 用相同密钥重新计算签名,和 Cookie 中的
sig严格比对(用hmac.Equal(),防时序攻击) - 确认
abs(time.Now().Unix() - ts) ,且 <code>time.Now().Unix() >= ts(拒绝未来时间)
漏掉任意一条,都应直接拒绝请求并清空该 Cookie(c.SetCookie(..., MaxAge: 0))。
别忽略中间件注册顺序和 echo.HTTPError 的处理细节
校验逻辑必须放在路由处理器之前,且不能被其他中间件(比如 JWT 解析)提前消费请求体或跳过。建议封装成独立中间件:
func AntiReplayMiddleware(secret string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cookie, err := c.Cookie("auth_token")
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "missing auth token")
}
if !isValidTimestamp(cookie.Value, secret) {
// 清除非法 Cookie
c.SetCookie(&http.Cookie{
Name: "auth_token",
MaxAge: 0,
Path: "/",
})
return echo.NewHTTPError(http.StatusUnauthorized, "replay or tampered token")
}
return next(c)
}
}
}这里容易踩坑:如果后续 handler panic,而你没配全局 HTTPErrorHandler,错误会被吞掉;另外 cookie.Value 是 URL 解码后的字符串,解析前别手动 url.QueryUnescape——c.Cookie() 已帮你做了。
真正难的不是算时间差,是让签名密钥安全轮换、让滑动窗口和客户端时钟误差达成平衡——这两个点,上线后得靠日志持续观察误拒率。


















