hmac.Equal是唯一安全的签名比对方式,必须用于恒定时间比较;需先hex解码客户端签名,统一返回401;hmac.New首参须为哈希构造函数(如sha256.New),不可加括号;body读取后须用io.NopCloser重置;timestamp和nonce须严格校验防重放;签名原文拼接顺序必须完全一致。

hmac.Equal 必须用,不能用 == 或 bytes.Equal 比较签名
Go 的字符串或字节切片比较不是恒定时间的。攻击者可通过响应延迟差异(微秒级)逐字节推断出合法签名——这是典型的时序攻击。hmac.Equal 内部强制遍历全部字节、屏蔽分支预测、抹平执行路径,是唯一安全的选择。
- 必须先用
hex.DecodeString把客户端传来的 hex 签名转成[]byte,再与本地计算的mac.Sum(nil)结果比对 - 若
hex.DecodeString失败(如长度非 64、含非法字符),直接拒绝请求,不进入比对逻辑 - 不要对“解码失败”和“比对失败”返回不同状态码或错误信息,统一返回
401 Unauthorized -
hmac.Equal要求两个参数都是[]byte;传字符串会编译报错,传nil可能导致逻辑漏洞(hmac.Equal(nil, nil)返回true)
hmac.New 的第一个参数必须是函数构造器,不是哈希实例
写成 hmac.New(sha256.New(), key) 会编译失败:因为 sha256.New() 返回的是 hash.Hash 实例,而 hmac.New 第一个参数需要的是类型为 func() hash.Hash 的函数指针。
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- ✅ 正确:
hmac.New(sha256.New, key)(注意sha256.New后无括号) - ❌ 错误:
hmac.New(sha256.New(), key)、hmac.New(sha256.Sum256{}, key)、hmac.New(sha256.New().Sum(nil), key) - 其他哈希同理:
sha512.New、sha3_256.New都不能加括号调用 - 如果用第三方哈希库(如
golang.org/x/crypto/sha3),也要确认其New是函数而非方法
io.ReadAll(r.Body) 后必须重置 r.Body,否则后续解析失败
HTTP 请求体是单次读取流,io.ReadAll(r.Body) 读完后,r.Body 就空了。后续调用 json.Unmarshal 或框架绑定(如 Gin 的 c.ShouldBindJSON)会得到 EOF 或空数据。
- 正确做法:读出原始字节 → 计算签名 → 用
io.NopCloser(bytes.NewReader(b))重建r.Body - 必须在任何业务层解析前完成重置,顺序不能颠倒
- 若 body 可能很大(>1MB),应限制大小:
http.MaxBytesReader包裹原始r.Body,防内存耗尽 - 不要用
c.GetRawData()(Gin):它只能调一次,且调用后c.Request.Body不可再用
timestamp 和 nonce 校验不严,等于没签名
HMAC 只保完整性和身份,不防重放。只校验签名值,攻击者截获一次请求就能无限重放。
- 时间戳必须双向校验:
req.Timestamp不能早于time.Now().Add(-5 <em> time.Minute).Unix()</em>,也不能晚于time.Now().Add(2 time.Minute).Unix()(防客户端时间快太多) -
nonce必须去重:存入 Redis 或本地 LRU cache(如github.com/hashicorp/golang-lru),key 建议带用户 ID 前缀("sig:nonce:" + userID + ":" + nonce) - 所有时间戳单位必须统一为秒(
time.Now().Unix()),别混用UnixMilli() - 如果客户端用毫秒时间戳,服务端也得用毫秒比对,但需提前除以 1000 并向下取整,否则精度不一致会导致误拒
最常被忽略的一点:签名原文拼接顺序和服务端必须一字不差——包括换行符、空格、大小写、URL 编码方式、字段是否为空。任何看似微小的差异(比如客户端用 url.QueryEscape,服务端用 url.PathEscape)都会让 hmac.Equal 返回 false,且很难定位。

















