refactor(sign): 抽取公共签名原语 signutil,三套签名串格式保持不变

商户侧 /api/client/v1、源头侧 /api/open/v1 与乐玩开放平台各有一套签名实现,
其中语义完全一致的部分重复了多份,任一处修复都容易漏改另外两处。

- 新增 internal/pkg/signutil,只收敛四类一致性原语:HMAC-SHA256→hex、
  body SHA256 摘要、时间戳偏差校验、常数时间比较
- 中间件与 lewan 改为调用该包,删除 middleware 里重复的 hmacSHA256Hex 与 abs64
- 刻意不统一「签名串构造」:三者拼接方式是对外契约且互不相同
  (商户:字典序 & + body 摘要;源头:字典序 & + body 原文;乐玩:换行 6 行),
  强行合并会直接改变握手结果
- 保留各处原有细节语义:商户侧十六进制签名大小写不敏感、乐玩签名原样比较、
  VerifyWebhook 在 allowedSkew<=0 时仍要求时间戳可解析

验证:三套方案既有端到端测试全部通过(含乐玩官方测试向量、大写签名、重放拒绝);
新增 signutil 已知向量与偏差/比较边界测试,以及 VerifyWebhook 边界语义测试。
This commit is contained in:
yml2213
2026-10-06 19:53:55 +08:00
parent 5ed36c724f
commit a47b61a98d
6 changed files with 320 additions and 58 deletions
@@ -2,15 +2,13 @@ package middleware
import (
"bytes"
"crypto/hmac"
"io"
"strconv"
"strings"
"time"
"affiliate_dash/internal/model"
"affiliate_dash/internal/pkg/openlog"
"affiliate_dash/internal/pkg/response"
"affiliate_dash/internal/pkg/signutil"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -73,8 +71,7 @@ func SourceOpenAuth(cfg SourceOpenAuthConfig) gin.HandlerFunc {
c.Abort()
return
}
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil || abs64(time.Now().Unix()-ts) > cfg.SkewSeconds {
if !signutil.TimestampWithinSkew(timestamp, time.Duration(cfg.SkewSeconds)*time.Second) {
openlog.Warn(c, "source_open_auth expired ts=%s skew=%d", timestamp, cfg.SkewSeconds)
response.Unauthorized(c, "请求已过期或 X-Timestamp 格式错误")
c.Abort()
@@ -92,7 +89,7 @@ func SourceOpenAuth(cfg SourceOpenAuthConfig) gin.HandlerFunc {
c.Set(openlog.CtxBody, string(bodyBytes))
expected := BuildOpenSign(apiKey, cfg.APISecret, timestamp, nonce, c.Request.Method, c.Request.URL.Path, string(bodyBytes))
if !hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected)) {
if !signutil.EqualHexSignatures(sign, expected) {
openlog.Warn(c, "source_open_auth sign_mismatch method=%s path=%s body=%s sign=%s expected=%s",
c.Request.Method, c.Request.URL.Path, openlog.Truncate(string(bodyBytes), 200),
openlog.MaskSign(sign), openlog.MaskSign(expected))