正确签名需确保密钥和待签数据字节完全一致:密钥用[]byte(勿字符串自动转)、数据格式(UTF-8无BOM、JSON序列化后、时间格式等)与对方严格对齐;校验必须用hmac.Equal,注意base64编解码和大小写。

怎么用 crypto/hmac 算出正确的签名
Go 的 hmac 不会自动帮你做编码或填充,直接拿原始字节算,结果和别人对不上?大概率是输入没对齐。比如对方用 UTF-8 字符串、你用了含 BOM 的字符串;或者对方先 json.Marshal 再签名、你直接签了结构体指针。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
-
hmac.New的密钥必须是[]byte,别传字符串进去再让函数自己转——Go 会按 UTF-8 转,但有些服务端用的是 raw bytes 密钥(比如 hex 解码后的 32 字节) - 待签名数据必须和对方完全一致:空格、换行、字段顺序、时间格式(
"2006-01-02T15:04:05Z"还是"2006-01-02 15:04:05")都要核对 - 别在签名前对数据做任何隐式处理,比如自动 trim 空格、忽略 nil 字段、排序 map 键——除非协议明确要求
校验时为什么 hmac.Equal 返回 false
常见错误是手写比较:if computed == received。这在 Go 里是字节切片地址比较,永远 false;或者用 bytes.Equal 但两边长度不等(比如对方 base64 编码后末尾有 =,你漏解了)。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 必须用
hmac.Equal做恒定时间比较,防止时序攻击——它内部已处理长度不等、内存布局差异等问题 - 如果对方传的是 base64 编码的签名,你得先用
base64.StdEncoding.DecodeString解出来,再跟本地算的[]byte比;别拿 base64 字符串和原始字节数组比 - 注意大小写:有些 API 要求签名全大写 hex,有些要小写;
fmt.Sprintf("%x", sum)是小写,"%X"才是大写
hmac.New 选哪个哈希函数才安全
hmac.New 第二个参数要传 hash.Hash,不是随便选 sha256.New 就行。密钥长度、输出长度、抗碰撞性都影响实际安全性。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 优先用
sha256:密钥 ≥ 32 字节时足够强,且几乎所有服务端都支持;sha1已被标记为不安全,别用 - 如果对方指定了
HMAC-SHA512,就用sha512.New,别图省事换成sha256——输出长度不同,base64 编码后长度也不同,校验必然失败 - 密钥太短(比如只有 8 字节)时,即使算法是 sha256,实际强度也会被拖累;建议至少 16 字节,生产环境 32 字节起
为什么本地能签、线上校验总失败
最常被忽略的是环境差异:本地开发机时区是 CST,CI 环境是 UTC,导致时间戳字段值不同;或者本地用 go mod vendor 锁死了某个旧版 crypto 库,而线上用的是系统自带 Go 版本。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 把签名逻辑封装成独立函数,并传入所有依赖项(如时间、随机数、编码器),方便打桩测试
- 在签名前后加日志,打印原始待签数据(用
fmt.Printf("%q", data)看不可见字符)、密钥长度、最终签名 hex,和对方提供样例逐字节比对 - 确认 Go 版本:低于 1.19 的
crypto/hmac在某些极端输入下有边界行为差异,升级到 1.20+ 更稳妥
真正卡住的时候,往往不是算法写错了,而是两边对“数据”和“密钥”的定义根本没对齐——多打一行日志,比重读三遍文档管用。


















