必须使用 hmac.new 而非拼接字符串生成签名,因 hmac 需密钥参与哈希过程;签名载荷应为 timestamp|method|path|bodyhash(utf-8 字节流),时间戳偏差需控制在 5 分钟内,secret 严禁硬编码或明文传入。

签名生成必须用 hmac.New 而不是手拼字符串
很多人直接把 secret + timestamp + body 拼成字符串再 sha256.Sum256,这完全不等价于 HMAC,攻击者可轻易伪造。HMAC 是密钥参与哈希过程的算法,必须用 Go 标准库的 hmac.New 构造器。
- 正确做法:用
hmac.New创建 hash 实例,Write写入待签名数据(如timestamp|method|path|body_hash),再Sum(nil) - 常见错误:对原始请求体直接
sha256.Sum256(body)后拼接,没用密钥参与计算,等于没签名 - body 必须标准化:空 body 传空字符串,JSON body 先格式化或取
sha256(body)哈希值(避免空格/换行差异) - 推荐签名载荷结构:
fmt.Sprintf("%s|%s|%s|%s", timestamp, method, path, bodyHash),全部用 UTF-8 字节流写入hmac.Hash
time.Now().UnixMilli() 和服务器时间偏差必须控制在 5 分钟内
签名里的时间戳不是为了“精确”,而是防重放。客户端和服务端时钟不同步超过窗口期,签名就必然失败 —— 这不是 bug,是设计使然。
- 服务端校验时用
abs(serverTime - reqTimestamp) > 300000(毫秒),超时直接拒收 - 不要用
time.Parse解析字符串时间戳再比对,避免时区/格式错误;要求客户端传毫秒级整数1717023456123 - 前端 JS 可通过 NTP 服务校准(如
time.cloudflare.com),但更简单的是首次请求先调用/api/time获取服务端时间差做补偿 - 注意:Go 的
time.Now().UnixMilli()在容器中可能受宿主机时钟漂移影响,建议宿主机启用systemd-timesyncd或chrony
验证时必须严格按顺序处理 header、body、query,且禁止修改原始字节
哪怕多一个空格、少一个换行、query 参数顺序调换,HMAC 就会完全不同。验证逻辑和签名逻辑必须字节级一致,否则 99% 的“验签失败”都出在这儿。
- query string 要按 key 字典序排序并拼接(如
a=1&b=2,不是b=2&a=1),URL decode 后再参与签名 - header 只取指定字段(如
X-Signature,X-Timestamp,X-Nonce),忽略大小写但值要原样(不 trim 空格) - body 读取一次后需用
io.NopCloser重新包装,否则后续 handler 读不到 —— 常见 panic:http: read on closed response body - 示例关键片段:
hash := hmac.New(sha256.New, []byte(secret)) hash.Write([]byte(fmt.Sprintf("%d|%s|%s|%s", ts, method, path, bodyHash))) expected := hex.EncodeToString(hash.Sum(nil))
secret 不要硬编码,也不要用环境变量明文传
secret 是签名体系的命门,泄露即全线失守。K8s Secret、Vault、或启动时解密加载才是正路,任何“临时方便”的做法都会在压测或日志里翻车。
立即学习“go语言免费学习笔记(深入)”;
- 禁止:
os.Getenv("API_SECRET")直接用 —— 日志、pprof、panic trace 都可能打出来 - 禁止:
flag.String("secret", "xxx", "...")—— ps aux 一眼可见 - 推荐:启动时从
/run/secrets/api_signing_key(Docker Swarm)或os.ReadFile("/vault/secrets/key")加载,内存中只存[]byte,不用 string - 额外提醒:别用同一个 secret 给所有客户端,应支持 per-app key,并在验证前查数据库或本地 map 判断是否启用/轮换中
签名逻辑看着简单,但 timestamp 窗口、body 归一化、secret 生命周期管理,三处任一松动,安全就归零。实际部署时,最常被绕过的不是算法,是开发顺手打的日志里那行 log.Printf("sign: %s", sign)。










