必须用 hmac.equal 做恒定时间比对,因 == 比较字符串会提前退出导致时序攻击;需先 hex.decodestring 将客户端签名转为 []byte,再与 mac.sum(nil) 结果比对,且 hmac.new 第一参数须为 sha256.new 而非 sha256.new()。

必须用 hmac.Equal 做签名比对,且比对前要 hex.DecodeString,否则直接 == 会引发时序攻击。
为什么不能用 == 比较签名字符串
Go 的字符串比较在遇到第一个不匹配字节时就提前返回,响应时间存在微秒级差异。攻击者可据此逐字节推断出合法签名——这是典型的时序攻击。
-
hmac.Equal是恒定时间比对:无论输入是否相等,执行路径和耗时都一致 - 常见错误是先
hex.EncodeToString(mac.Sum(nil))得到字符串,再用==判断——这完全破坏安全前提 - 正确做法:客户端传来的 hex 签名(如
"a1b2c3...")必须先hex.DecodeString(signature)成[]byte,再与mac.Sum(nil)的原始摘要比对 - 若
hex.DecodeString失败(长度非 64、含非法字符),应直接拒绝请求,不进入比对逻辑
hmac.New 的第一个参数不能加括号
写成 hmac.New(sha256.New(), key) 会编译失败,因为 hmac.New 要求的是函数指针,不是哈希实例。
- ✅ 正确:
hmac.New(sha256.New, key)(sha256.New后无括号) - ❌ 错误:
hmac.New(sha256.New(), key)、hmac.New(sha3_256.New(), key)、hmac.New(sha256.Sum256{}, key) - 其他哈希同理:
sha512.New、sha3_256.New都不能加括号调用 - 密钥必须是
[]byte,别用string("key")强转——底层内存不共享,隐式转换可能出错
HTTP body 只能读一次,必须缓存并重置 r.Body
签名中间件读完 r.Body 后,后续 handler 再读就是空的。这不是 bug,而是 HTTP 流的设计特性。
- 必须用
io.ReadAll(r.Body)一次性读出原始字节 - 立即重置:
r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - 顺序不能颠倒:必须在任何业务解析(如
c.ShouldBindJSON)前完成重置 - 大文件场景需限制大小:
http.MaxBytesReader包裹原始r.Body,防内存耗尽 - 别用
c.GetRawData()(Gin):它只能调一次,且调用后c.Request.Body不可再用
防重放必须原子校验 timestamp 和 nonce
只检查时间窗口或只查 nonce 是否存在,都不足以防重放。两者必须同时满足,且校验过程不能有竞态。
- 时间戳统一用秒级:
time.Now().Unix(),客户端和服务端都不能用毫秒 - 允许偏差建议 ≤300 秒,同时拒绝未来时间过长的请求(如
> time.Now().Add(2*time.Minute).Unix()) -
nonce必须全局唯一,建议用 16 字节随机值 +hex.EncodeToString - 去重必须原子:Redis 用
SET key value EX 360 NX,数据库用INSERT ... ON CONFLICT DO NOTHING - 避免用
sync.Map存 nonce 做去重——跨进程无效,只适合单机调试
最常被忽略的点是签名原文拼接顺序:method、path、timestamp、nonce、body 必须和服务端文档严格一致,任意换行、空格、编码差异都会导致验签失败。调试时建议在中间件开头打印脱敏后的待签名字符串,方便比对。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











