应使用 hmac.new 而非手拼字符串签名,签名原文需含 http 方法、uri 路径、标准化 query(字典序、url.pathescape 编码)、x-signature-timestamp 和 x-signature-nonce;结果用 hex.encodetostring 输出小写十六进制;验签须校验时间戳(≤300秒)和 nonce(redis 去重);推荐用自定义 roundtripper 统一处理,避免手动重复;query 编码须自行实现,禁用 url.values.encode。

签名用 hmac.New 而不是手拼字符串
很多人直接把参数按字典序拼接再加 secret 做 md5 或 sha256,这既不安全也不符合标准签名逻辑。HMAC 是带密钥的哈希,必须用 hmac.New 构造,否则攻击者容易重放或篡改参数。
实操建议:
- 用
hmac.New配合sha256.New,密钥用[]byte形式传入,别转成字符串再转回 - 签名原文必须包含:HTTP 方法、URI 路径(不含 query)、标准化的 query string(key=value&key=value,无空格,value 严格 URL 编码)、
X-Signature-Timestamp头(秒级时间戳)、X-Signature-Nonce(随机 16 字符) - 不要对整个
req.URL.String()签名——它含 query,但 query 顺序不可控;应手动解析并排序键值对 - 签名结果用
hex.EncodeToString输出小写十六进制字符串,别用 base64(服务端验签时编码不一致就失败)
验签时必须检查 X-Signature-Timestamp 和 X-Signature-Nonce
只比对签名值等于开门放行,等于没设防。时间戳超 300 秒即拒收,防止重放;nonce 要存 Redis 做去重(TTL 设为 300 秒),内存 map 不适合多实例部署。
常见错误现象:
立即学习“go语言免费学习笔记(深入)”;
- 时间戳用
time.Now().Unix()但服务端用time.Now().UTC().Unix(),时区差导致总验不过 - nonce 存在本地 map,扩容后新实例收不到旧请求的 nonce 记录,误判为重放
- 没校验 timestamp 是否为纯数字字符串,攻击者传
X-Signature-Timestamp: 123abc可能 panic - 验签前没调用
req.ParseForm(),导致req.FormValue返回空,签名原文错位
net/http.RoundTripper 拦截请求做自动签名最省心
每个业务函数里手动算签名、塞 header、处理错误,重复代码多还容易漏。用自定义 RoundTripper 把签名逻辑下沉到 HTTP 客户端层,调用方完全无感。
使用场景:
- 内部微服务间调用统一鉴权
- SDK 封装对外 API 客户端(如封装支付网关 SDK)
- 需要同时支持多种签名算法(如 HMAC-SHA256 / ECDSA)时,靠接口注入切换
注意点:
- 不能在
RoundTrip里修改原始*http.Request.Body(已关闭或不可读),需用io.NopCloser(bytes.NewReader(bodyBytes))重建 - 签名头要加在
req.Header.Set,别用Add,避免重复 - 如果用了
context.WithTimeout,记得把 context 传给http.DefaultTransport.RoundTrip,否则超时控制失效
Go 标准库 url.Values 编码不满足签名要求,得自己实现
url.Values.Encode() 会把空格转成 +,而签名规范要求空格必须是 %20;还会对斜杠 / 编码,但路径中斜杠不该编码。直接用它生成签名原文,服务端一验就挂。
实操建议:
- 遍历
req.URL.Query()的 key-value,对每个 value 单独调用url.PathEscape(它把空格变%20,且不碰/) - key 不需要 escape,但必须按字典序排序后拼接,用
strings.Join拼key=value对,再用&连接 - 路径部分只取
req.URL.EscapedPath(),别用req.URL.Path(它不保证已解码) - 如果 query 中有重复 key(如
filter=a&filter=b),签名原文必须保留全部,不能去重
复杂点在于签名原文构造和验签原文必须完全一致——连换行、空格、编码方式都不能差一点。线上出问题,90% 是客户端和服务端的 query 排序或编码逻辑有一处没对齐。










