签名必须包含时间戳和随机盐值,时间戳用UnixMilli()并限制±15秒有效期,盐值由crypto/rand.Read()生成16字节后hex编码,拼接顺序为method|path|body|timestamp|nonce,验证端须先校验时间窗口再查nonce去重(存Redis),密钥应为32字节且安全加载,生成与校验逻辑须无状态分离。

签名字符串生成必须包含时间戳和随机盐值
不加时间戳的签名无法防御重放攻击,不加盐值的签名容易被字典穷举。Go 标准库 crypto/hmac 本身不处理这些字段,得自己拼接。常见错误是把时间戳直接用 time.Now().Unix(),导致精度太粗(秒级),攻击者有整整一秒窗口;正确做法是用 time.Now().UnixMilli(),并限制有效期(比如 ±15 秒)。盐值不能硬编码,也不能用固定字符串,建议从 crypto/rand.Read() 生成 16 字节随机字节,再 hex 编码为字符串参与拼接。
拼接顺序很重要:通常按 method|path|body|timestamp|nonce 排列(注意分隔符统一用 |,避免 URL 编码干扰)。body 若为空则留空字符串,不能省略字段。示例片段:
data := fmt.Sprintf("%s|%s|%s|%d|%s", method, path, body, ts, nonce)验证端必须严格校验时间戳偏差与 nonce 去重
服务端收到签名后,第一件事不是算 HMAC,而是先解析并检查 timestamp 是否在允许窗口内(如 abs(ts - now.UnixMilli()) > 15000),超时直接拒绝。第二步才是查 nonce 是否已存在——这里容易踩坑:很多人用内存 map 存 nonce 做去重,但单机部署没问题,多实例时就失效。实际应存到共享存储(如 Redis),key 为 nonce:{nonce},过期时间设为略大于时间窗口(如 20 秒),并用原子 setnx + expire 保证写入安全。
验证时若发现 nonce 已存在,说明是重放请求,立刻返回 401;若时间戳合法但签名不匹配,不要透露是“签名错”还是“nonce 重复”,统一返回 401,避免信息泄露。
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
使用 hmac.New() 时密钥必须固定长度且不可导出
密钥长度影响 HMAC 安全性:hmac.New() 底层对密钥做预处理,若密钥太短(如少于 32 字节),会被内部填充补零,削弱抗碰撞能力;太长(如超 64 字节)则先 hash 再用。推荐直接用 32 字节密钥:从 crypto/rand.Read() 生成,或用 sha256.Sum256([]byte("your-secret-salt")) 衍生。绝对不要把密钥写死在代码里,更不要通过环境变量明文传——应由配置中心注入加密后的密文,启动时解密到内存,且禁止日志打印、dump 或反射读取。
示例初始化方式:
key := make([]byte, 32) rand.Read(key) // 实际中替换为安全的密钥加载逻辑 h := hmac.New(sha256.New, key)
双向验证需分离签名生成与校验逻辑,避免共享可变状态
很多实现把签名生成器封装成 struct 并带 field 存密钥、salt 等,结果在校验时误复用同一实例,导致并发下 nonce 或临时 buffer 被污染。正确做法是:生成签名用无状态函数(输入全靠参数),校验也用纯函数(输入含完整请求数据 + 签名头 + 密钥),两者不共享任何指针或全局变量。尤其注意 bytes.Buffer 或 strings.Builder 不要复用,每次调用都新建。
另一个易忽略点:HTTP header 中的签名字段名要约定清楚(如 X-Signature 和 X-Timestamp、X-Nonce),客户端和服务端必须完全一致,大小写敏感,且服务端解析前要 trim 空格——否则 " abc123 " 和 "abc123" 会被当成不同 nonce。

















