不能只比对 hmac.Sum() 的字节切片,因其返回带额外容量的 []byte,== 比较会因底层数组长度/容量差异误判;且若明文拼接未统一编码(URL 编码、空格、换行、字段顺序),客户端与服务端摘要必然不一致。

签名验证为什么不能只比对 hmac.Sum() 的字节切片
Go 的 hmac.Sum() 返回的是一个带额外容量的 [].byte,直接用 == 比较会因底层数组长度/容量差异导致误判;更关键的是,明文拼接后计算 HMAC 时若未严格统一编码(如 URL 编码、空格、换行、字段顺序),服务端和客户端算出的摘要必然不一致。常见错误是客户端用 url.Values.Encode() 拼 body,而服务端用 io.ReadAll() 读原始字节,二者在空格、换行、键值顺序上天然不同。
- 始终用
hmac.Sum(nil).Bytes()获取干净字节切片,避免隐式扩容干扰 - 签名前所有参与计算的字段(method、path、query、headers、body)必须按固定规则序列化,推荐 JSON 序列化或 RFC 3986 编码 + 字典序排序
- HTTP header 只取指定白名单(如
X-Request-Timestamp、X-Request-Nonce),且 key 全小写、value 去首尾空格后拼接
如何安全提取并标准化请求 body 用于签名
HTTP 请求 body 是 io.ReadCloser,只能读一次。如果在中间件里先读一遍验签,后续 handler 就收不到 body。正确做法是用 httputil.DumpRequestOut() 或手动缓存:读取后用 bytes.NewReader() 重置 body,并调用 req.Body = ioutil.NopCloser()(Go 1.16+ 改为 req.Body = io.NopCloser())。但注意:若 body 过大(如文件上传),应跳过 body 签名,仅签 headers 和 path。
- 小 body(io.ReadAll(req.Body) 读取,保存为
bodyBytes,再用req.Body = io.NopCloser(bytes.NewReader(bodyBytes))复位 - 大 body 或流式请求:签名逻辑中显式忽略
req.Body,只基于req.Method、req.URL.Path、req.URL.RawQuery和白名单 headers 构造签名原文 - 避免对
Content-Encoding: gzip的 body 直接签名——应先解压再签,或约定签名发生在压缩前
Go 标准库 hmac 验证时如何防时序攻击
hmac.Equal() 是 Go 提供的恒定时间比较函数,必须用它替代 == 或 bytes.Equal()。哪怕只是校验签名字符串是否相等,只要涉及密钥派生或摘要比对,就必须走恒定时间路径。否则攻击者可通过响应延迟差异暴力推断签名字符。
- 签名比对必须用
hmac.Equal(expectedSig, actualSig),其中两个参数都是[]byte - 不要把签名 base64 编码后再比对——base64 解码本身不是恒定时间,应先 base64 解码为 bytes,再用
hmac.Equal() - 时间侧信道不止存在于签名比对:若验签失败立即返回 401,而签名格式错误(如缺失 header)返回 400,也可能暴露信息,建议统一返回 401 + 模糊提示
签名头字段命名与传输的兼容性陷阱
HTTP header 名称区分大小写,但 Go 的 http.Header 内部用 canonical key(如 X-Request-Signature → X-Request-Signature),而某些代理或 CDN 会强制转为小写。更麻烦的是,部分客户端(如旧版 curl)发送 X-Request-Signature,服务端收到却是 x-request-signature,用 req.Header.Get("X-Request-Signature") 就取不到。
- 统一用 Go 的 canonical 形式定义常量:
const SigHeader = "X-Request-Signature",取值时用req.Header.Get(SigHeader) - 若需兼容弱规范客户端,可 fallback 到遍历
req.Headerkeys 并忽略大小写匹配(用strings.EqualFold()) - 签名原文中 header 字段名也必须统一小写(如
"x-request-timestamp"),避免大小写不一致导致摘要错配
time.Now().Unix() 对比 header 中的 X-Request-Timestamp。


















