签名生成必须固定参数顺序和编码规则:先对map键排序,再按key=value拼接并URL编码value;使用HMAC-SHA256算法,密钥从配置读取;校验需防重放和参数污染,严格按白名单提取参数。

签名生成逻辑必须固定参数顺序和编码规则
Go 里最常踩的坑是直接对 map[string]string 遍历拼接,但 Go 的 map 遍历顺序不保证一致,会导致同一组参数每次生成的签名不同。必须先将参数 key 排序,再按 key=value 格式拼成字符串,且 value 要做 URL 编码(用 url.QueryEscape,不是 url.PathEscape)。
常见错误现象:本地测试签名总校验失败,抓包对比发现拼接字符串顺序乱了;或特殊字符(如空格、中文、+)没正确编码,服务端解码后参数值已变。
- 所有参与签名的参数必须明确约定字段白名单(比如排除
sign、timestamp可能也要排除,视业务而定) - 时间戳建议用
time.Now().Unix(),单位秒,避免毫秒导致前后端时区/精度不一致 - 拼接前统一转小写 key(如果协议要求),并过滤掉空值或零值
""字符串 - 最终拼接格式示例:
app_id=123&method=pay&nonce=abc×tamp=1715678901
签名算法选型:HMAC-SHA256 是最稳妥的选择
别自己实现 MD5 或 SHA1 —— 这些已被证明不安全,且很多平台明确拒绝。HMAC-SHA256 是事实标准,Go 标准库 crypto/hmac 和 crypto/sha256 支持开箱即用,性能好、无依赖。
注意密钥处理:不要硬编码在代码里,应从环境变量或配置中心读取;密钥本身不能含控制字符或换行,否则 hmac.New 不报错但结果异常。
- 签名原文(即上一步拼好的字符串)必须原样传入
hmac.Write,不能额外加换行或空格 - 输出用
hex.EncodeToString转成小写十六进制字符串(有些平台要求大写,需确认) - 示例关键片段:
h := hmac.New(sha256.New, []byte(secretKey))<br>h.Write([]byte(sortedQuery))<br>return hex.EncodeToString(h.Sum(nil))
校验函数要防御重放和参数污染
签名通过只是第一步。攻击者可能截获合法请求反复重放,所以必须校验 timestamp 是否在有效窗口内(如 ≤ 300 秒);同时要防止恶意添加未授权参数(比如 user_id=admin),校验前必须严格按白名单提取参数,丢弃多余字段。
常见错误:只校验签名,不检查时间戳有效期;或从 r.FormValue 直接取参,没做参数清洗,导致空格绕过、URL 编码混淆等。
- 用
r.URL.Query()或r.PostForm获取原始参数,避免中间件提前修改 - 校验前先检查
timestamp是否为合法数字、是否超出当前时间 ±5 分钟 - 若接口支持 GET 和 POST 混合传参,需合并两处参数并去重(同 key 以 POST 优先或按协议约定)
- 签名字段
sign必须从参数中移除后再参与计算,否则递归死循环
HTTP 中间件里集成签名校验要区分错误类型
别把所有校验失败都返回 401,这会掩盖真实问题。应该区分:签名错误(401)、时间超时(400 + 自定义 error code)、参数缺失(400)、非法参数(403)。方便前端或运维快速定位。
容易被忽略的是:GET 请求的 query 参数和 POST 表单参数可能同时存在,而 ParseForm() 默认只解析一次,多次调用会 panic。务必在中间件开头就调用一次 r.ParseForm()。
- 校验失败时,记录原始请求参数和生成的待签字符串(脱敏后),用于事后审计
- 生产环境禁用打印完整密钥或原始签名字符串的日志
- 如果使用 Gin/Echo 等框架,注意其自动绑定可能改变参数原始形态(如自动 trim 空格),校验应放在绑定之前

















