签名验证中间件必须全局注册于路由匹配前,预读并重置request.body以支持多次读取,算法需与客户端严格一致,密钥禁止硬编码,错误响应应泛化且跳过options预检。

签名验证中间件必须在路由匹配前注册
Echo 的中间件执行顺序依赖注册时机,echo.Use() 添加的全局中间件会在所有路由处理器之前运行,而 echo.Group().Use() 或路由级 Use() 则只作用于子路径。如果你把签名验证放在 GET /api/data 的 handler 里再校验,就已错过请求头读取和 body 消费的最佳时机——body 可能已被多次读取或关闭。
正确做法是用 e.Use(verifySignatureMiddleware) 全局注册,确保每个请求都先过签名校验:
func verifySignatureMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sig := c.Request().Header.Get("X-Signature")
timestamp := c.Request().Header.Get("X-Timestamp")
if sig == "" || timestamp == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "missing signature or timestamp")
}
// 后续校验逻辑...
return next(c)
}
}
校验前必须预读并重置 request.Body
Echo 默认不缓存请求体,c.Request().Body 是一个单次读取流。一旦你调用 ioutil.ReadAll(c.Request().Body)(或 echo.Context.Bind()),body 就被消费完毕,后续 handler 再读就会得到空数据,导致业务逻辑失败。
解决方法是:用 io.ReadCloser 包装原始 body,并在验证后用 bytes.NewReader() 重建可重读的 body:
- 调用
io.ReadAll(c.Request().Body)获取原始字节 - 校验通过后,用
c.Request().Body = io.NopCloser(bytes.NewReader(rawBody))恢复 body - 注意:不要漏掉
defer c.Request().Body.Close(),否则可能泄漏连接
签名算法必须与客户端严格一致
常见坑是客户端用 HMAC-SHA256 签名,服务端却用 md5 或漏掉排序参数;或时间戳单位不一致(秒 vs 毫秒),导致 abs(now - timestamp) > 300 校验失败。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
典型安全校验应包含:
- 时间戳有效期检查(建议 ≤ 300 秒)
- 签名原文拼接规则:如
method + \n + path + \n + timestamp + \n + sorted_query_string + \n + sorted_body_json - 密钥必须从环境变量或配置中心加载,禁止硬编码在代码中
- 推荐使用
hmac.New(sha256.New, []byte(secretKey))而非自定义字符串拼接哈希
错误响应要避免泄露敏感信息
签名失败时返回 401 Unauthorized 是合理的,但不能返回类似 "invalid signature: expected xxx got yyy" 这类提示——这等于告诉攻击者当前密钥派生方式或哈希长度。
统一返回泛化错误更安全:
return echo.NewHTTPError(http.StatusUnauthorized, "request unauthorized")
日志中可记录详细原因(如签名不匹配、超时、body 解析失败),但绝不透出到 HTTP 响应体。
另外,别忽略 OPTIONS 预检请求:如果前端发 CORS 请求,浏览器会先发 OPTIONS,此时若中间件强制校验签名,会导致预检失败。建议在中间件开头加判断:if c.Request().Method == "OPTIONS" { return next(c) }。










