必须在中间件中第一时间读取原始请求体并重置r.body,否则后续验签因body已eof而失败;签名头/参数需按协议从header、query或body中提取;原文拼接须严格按顺序、编码和格式;验签依赖go标准库,密钥、哈希、填充模式须与签名方完全一致。

如何用 Gin 获取原始请求体来验签
Gin 默认会提前读取并解析 body(比如自动绑定 JSON),导致后续再读 r.Body 时返回空——这是验签失败最常见原因。必须在中间件里第一时间读取原始字节,并重新包装成 io.ReadCloser 供后续使用。
- 在注册路由前插入中间件,用
r.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))恢复 body 流 - 不要在
ShouldBind或BindJSON之后才尝试读 body,此时已 EOF - 注意:Go 1.19+ 中
ioutil已弃用,改用io和bytes,例如bytes.NewReader(bodyBytes)
签名头和参数提取的典型位置
签名通常放在请求头(如 X-Signature)、URL 查询参数(sign=xxx)或独立字段(如 JSON 中的 signature 字段)。Gin 不提供统一验签接口,需按实际协议约定手动提取。
- Header 方式:
r.Header.Get("X-Signature"),注意大小写不敏感但建议按规范写全大写 - Query 方式:
r.FormValue("sign"),适用于 GET 请求或简单场景 - Body 内嵌方式:先解出原始 body,再用
json.Unmarshal提取signature字段,但要确保 body 未被提前消费
验签时拼接原文的常见陷阱
签名原文(message)拼接顺序、编码、分隔符稍有差异就会导致验签失败。多数服务端要求按固定字段顺序拼接,且不带空格、换行、多余引号。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 避免直接用
fmt.Sprintf拼接结构体,应显式按协议顺序取值:timestamp + "|" + nonce + "|" + bodyStr - body 原文必须是未格式化的原始字节(即网络传输时的 exact bytes),不是
json.MarshalIndent后的结果 - 中文或特殊字符务必确认是否需要 UTF-8 编码后再参与计算;部分旧协议要求 GBK,需提前转换
- 时间戳字段若来自 header(如
X-Timestamp),要校验其是否在合理窗口内(±5 分钟),防重放
用 crypto/rsa 或 crypto/ecdsa 验证签名
Gin 本身不处理密码学运算,验签逻辑完全依赖 Go 标准库。RSA 和 ECDSA 的验证方式不同,密钥格式(PEM/DER)、哈希算法(SHA256/SHA512)、填充模式(PKCS#1 v1.5 / PSS)都必须与签名方严格一致。
- RSA 验证示例:
rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], sigBytes),注意hashed是对原文哈希后的结果 - ECDSA 验证需先用
ecdsa.Verify,但输入是两个整数r、s,需从 ASN.1 编码中解析(可用crypto/subtle.ConstantTimeCompare防侧信道) - 公钥加载推荐用
crypto/x509.ParsePKIXPublicKey,而非ParsePKCS1PublicKey,兼容性更好 - 验签失败时错误信息通常是
crypto: verification error,不代表密钥错,更可能是原文或签名字节不对
验签逻辑一旦涉及时间窗口、nonce 缓存、密钥轮换,复杂度就迅速上升。别把验签中间件写成“万能适配器”,每个业务接口的签名规则往往略有差异,硬套通用函数反而容易漏掉关键校验点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










