Go生成HMAC-SHA256签名需严格按字典序拼接参数、URL编码值、密钥转[]byte;验证失败多因原始数据不一致,如时间戳偏差、头字段名错误、double-encode等。

Go 语言里怎么生成标准 HMAC-SHA256 签名
签名本质是用密钥对请求参数做确定性哈希,Go 自带 crypto/hmac 和 crypto/sha256 就够用,不需要第三方库。
关键不是“会不会调”,而是参数拼接顺序、编码方式、密钥处理这三处极易出错:
- 请求参数必须按字典序(key 升序)拼成
a=1&b=2&c=3形式,不能用 map 遍历顺序 —— Go 的 map 是随机遍历的 - 所有参数值必须先做 URL 编码(
url.QueryEscape),空格变%20而不是+,否则和服务端不一致 - 密钥不能直接传字符串,得用
[]byte(secret);如果服务端用 base64 解码密钥,你也得先base64.StdEncoding.DecodeString
示例片段:
h := hmac.New(sha256.New, []byte("your-secret"))
h.Write([]byte("a=1&b=2×tamp=1712345678"))
signature := hex.EncodeToString(h.Sum(nil))
验证签名时为什么老是 401?
90% 的验证失败不是算法错了,是请求体和服务端看到的“原始数据”不一致。
立即学习“go语言免费学习笔记(深入)”;
常见断点位置:
- 客户端把
Content-Type: application/json的 body 当作签名原文,但服务端可能只签 query 参数,或要求把 JSON 字段扁平化后参与排序 - 时间戳校验没关:服务端拒绝
timestamp超过 300 秒的请求,而你本地时间没和 NTP 同步,差了 2 分钟 - 签名头字段名写错,比如服务端认
X-Signature,你发了X-Api-Signature,连校验逻辑都没进 - 用了
http.Request.URL.RawQuery拼参,但里面已含编码,再套一次url.QueryEscape就 double-encode 了
要不要把签名逻辑封装成中间件?
要,但别在 http.Handler 里直接解析 body —— 会消耗 request body 流,后续 handler 读不到。
安全做法是只签 URL 和 header 中的固定字段(如 timestamp、nonce),body 留给业务层自己决定是否参与签名:
- 如果是 JSON API,建议让业务层把
json.Marshal后的字节切片传给签名函数,避免结构体字段顺序/omitempty 导致 hash 不一致 - 不要在中间件里调
r.Body.Read,改用io.TeeReader+bytes.Buffer复制流,代价可控 - 签名验证失败时,返回明确错误如
"invalid signature: timestamp expired",别笼统写"unauthorized",方便前端快速定位
不同语言签名结果不一致?重点查这三点
跨语言联调时,签名对不上基本是底层细节没对齐:
- Go 默认用
hex.EncodeToString输出小写十六进制,Python 的hmac.hexdigest()也是小写,但有些 Java 库默认大写 —— 统一转小写再比对 - Node.js 的
crypto.createHmac('sha256', key).update(data).digest('hex')里key如果是字符串,内部按 UTF-8 编码;Go 里[]byte("key")也是 UTF-8,没问题;但如果 Java 用SecretKeySpec且指定了ISO-8859-1,就必然错 - 空值处理:Go 的
url.Values{"a": []string{""}}拼出来是a=,PHP 的http_build_query默认忽略空值,变成a缺失 —— 必须约定空值显式传a=
最省事的调试方式:在双方都打日志,输出「参与签名的原始字符串」和「最终 hex 签名」,逐字符比对,别猜。


















