签名验证中间件核心逻辑是:在路由匹配后、controller执行前,通过middleware安全读取并复用原始request.body,构造规范待签字符串(含方法、路径、排序参数、body字节、时间戳等),用hmac-sha256比对header或query中的签名,失败立即401终止,成功才调ctx.next()。

签名验证中间件的核心逻辑是什么
Iris 中实现请求签名验证,本质是拦截 ctx.Request() 的原始字节流(而非调用 ctx.FormValue() 或 ctx.GetBody() 之后的数据),在解析前完成 HMAC-SHA256(或其他算法)比对。一旦调用过读取 Body 的方法,底层 http.Request.Body 就会被消耗,后续再读会返回空 —— 这是绝大多数人踩坑的起点。
- 签名通常放在 Header(如
X-Signature)或 Query(如sign) - 待签原文应包含:HTTP 方法 + 请求路径 + 排序后的查询参数 + (可选)原始 JSON Body 字节 + 时间戳/nonce
- 密钥必须安全存储,绝不能硬编码在中间件函数里
如何安全读取并复用 Request.Body
Iris 默认不缓存 Body,需手动用 ctx.Request().Body 读取一次、保存、再设回。但注意:Iris 的 ctx.Request().Body 是 io.ReadCloser,直接赋值会破坏关闭逻辑。
body, _ := io.ReadAll(ctx.Request().Body) ctx.Request().Body.Close() // 必须显式关闭原 Body ctx.Request().Body = io.NopCloser(bytes.NewBuffer(body))
- 必须在中间件开头立即执行,否则后续路由 handler 可能已触发读取
- 若使用
ctx.PostValue()或ctx.ReadJSON(),它们内部会再次读 Body → 此时 body 已是空字节,导致解析失败 - 更稳妥的做法是:只在验证阶段读一次,验证通过后不再动 Body,让后续 handler 自行处理
签名比对时常见错误有哪些
最常出错的是待签字符串构造不一致,导致服务端和客户端算出的签名永远不匹配:
- 查询参数未按 key 字典序排序(如
a=1&c=3&b=2要重排为a=1&b=2&c=3) - 忽略空值参数(有些 SDK 会自动过滤
key=这类空值,服务端没过滤就对不上) - Body 字节包含不可见字符(如换行、BOM)、或客户端发的是 Form 表单但服务端误当 JSON 解析
- 时间戳校验窗口太小(如只允许 ±30 秒),而客户端和服务端时钟不同步
示例关键片段:
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(method + path + sortedQuery + string(bodyBytes) + timestamp))
expected := hex.EncodeToString(h.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signatureHeader)) {
ctx.StatusCode(401)
ctx.WriteString("invalid signature")
return
}
要不要把签名验证做成 Iris 内置的依赖注入?
可以,但没必要。Iris 的 ctx.Values().Set() 已足够传递验证结果(如 ctx.Values().Set("auth_user_id", userID)),后续 handler 直接取用即可。
- 不建议用
app.RegisterDependency()注入签名结果,因为验证失败要中断流程,而依赖注入无法控制执行时机或提前终止 - 若需多处复用,封装成函数比注册依赖更清晰:
VerifyRequestSignature(ctx, secretKey) (bool, int64, error) - 注意:中间件中不要调用
ctx.Next()后再做验证,必须前置 —— 验证失败就得立刻返回,不能放行
签名验证真正难的不是代码,而是客户端与服务端对「待签原文」的定义是否完全一致;哪怕一个空格、一个排序规则差异,都会让签名失效。调试时优先打印两端生成的待签字符串做比对,而不是先怀疑密钥或算法。











