签名校验必须在 http 中间件中统一实现,而非分散于各 handler;需预读并重置 body、使用 hmac-sha256、校验时间戳与签名字段、密钥从环境加载,并覆盖时钟漂移等边界场景测试。

签名校验必须在 HTTP 中间件里做,而不是 handler 里
签名校验逻辑一旦写进具体 handler,就会重复、难维护、易遗漏。Golang 的 http.Handler 中间件才是统一拦截的合理位置——它天然覆盖所有路由,且能提前拒绝非法请求,避免后续业务逻辑执行。
常见错误是把签名验证塞进某个 API 函数里,结果新增接口时忘了加,或者用 if !valid { return } 手动判断,既分散又容易绕过。
- 中间件应返回标准
http.HandlerFunc,用next.ServeHTTP(w, r)向下传递合法请求 - 校验失败必须直接写响应并 return,不能只 log 或 panic,否则 handler 仍会执行
- 建议在中间件开头就调用
r.ParseForm()和/或io.ReadAll(r.Body)(注意 body 只能读一次),把原始请求数据固化下来供签名计算
签名算法要用 HMAC-SHA256,密钥绝不硬编码
MD5、SHA1 已不安全;单纯拼接字符串再哈希(如 sha256(secret + ts + data))易受长度扩展攻击;而 hmac.New 提供标准、抗碰撞的密钥派生方式。
密钥必须从环境变量或 secret manager 加载,绝不能出现在代码里或 git 历史中。生产环境若用 os.Getenv("API_SECRET"),至少要配合非空校验和 panic 提示。
- 签名原文必须包含:时间戳
ts(单位秒)、过期窗口(如 300 秒)、请求方法、路径、排序后的 query 参数、规范化后的 body(如 JSON 字段按 key 排序、无空格) - 推荐把签名字段放在 header,如
X-Signature,避免 query 或 body 被代理/网关篡改或丢弃 - 时间戳校验必须严格:服务端时间与客户端时间偏差 > 300 秒即拒收,防止重放
body 签名必须预读并重置,否则下游 handler 读不到
Golang 的 http.Request.Body 是单次读取流,中间件里一读就空了,后面 handler 再读会得到空字节。这是最常踩的坑,导致业务逻辑拿不到 POST 数据。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
正确做法是:用 io.ReadAll(r.Body) 读取原始 body,计算签名后,用 bytes.NewReader(bodyBytes) 新建一个可重读的 io.ReadCloser 赋给 r.Body。
- 注意 body 可能为空(GET 请求),需判空避免 panic
- 大文件上传场景慎用此法——内存暴涨。此时应改用 streaming signature(如分块哈希),但普通 API 不需要
- 若 body 是 form-data(
multipart/form-data),签名通常只覆盖 text 字段,文件部分不参与,需提前解析并分离
测试签名中间件时,必须覆盖时钟漂移和篡改边界
本地跑通不代表线上可用。真实环境中,客户端设备时间不准、NAT 网关改写 header、CDN 缓存旧请求,都会触发签名失败。
单元测试里要 mock time.Now(),构造 ts 偏移 ±301 秒、修改任意一个签名字段、替换 X-Signature 值,验证是否 401 拒绝。
- 用
httptest.NewRequest构造完整请求,手动设置Header.Set("X-Signature", ...)和FormValue或Body - 别只测“正确签名”,重点测“错一位字符”“少一个参数”“ts 提前 5 分钟”这三类典型失败
- 签名生成代码和校验代码必须共用同一套规范化逻辑(比如 query 排序函数),否则两边对不上
签名本身不难,难的是所有环节严丝合缝:时间同步、body 复用、header 传递、密钥隔离、测试覆盖。漏掉任意一环,都可能让防御形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










