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:
@@ -2,17 +2,14 @@ package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
"affiliate_dash/internal/pkg/openlog"
|
||||
"affiliate_dash/internal/pkg/response"
|
||||
"affiliate_dash/internal/pkg/signutil"
|
||||
"affiliate_dash/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -71,8 +68,8 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
||||
if err != nil || abs64(time.Now().Unix()-ts) > cfg.SkewSeconds {
|
||||
// 时间戳非法或超出偏差窗口都视为过期;解析与偏差判断统一由 signutil 负责。
|
||||
if !signutil.TimestampWithinSkew(timestamp, time.Duration(cfg.SkewSeconds)*time.Second) {
|
||||
openlog.Warn(c, "open_auth expired app_key=%s ts=%s skew=%d", openlog.MaskKey(appKey), timestamp, cfg.SkewSeconds)
|
||||
response.Unauthorized(c, "请求已过期或 X-Timestamp 格式错误")
|
||||
c.Abort()
|
||||
@@ -113,10 +110,9 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc {
|
||||
method := strings.ToUpper(c.Request.Method)
|
||||
path := c.Request.URL.Path
|
||||
expected := BuildOpenV1Sign(secret, appKey, timestamp, nonce, method, path, bodyBytes)
|
||||
if !hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected)) {
|
||||
bodyHash := sha256.Sum256(bodyBytes)
|
||||
if !signutil.EqualHexSignatures(sign, expected) {
|
||||
openlog.Warn(c, "open_auth sign_mismatch app_key=%s method=%s path=%s body_sha256=%s sign=%s expected=%s",
|
||||
openlog.MaskKey(appKey), method, path, hex.EncodeToString(bodyHash[:]),
|
||||
openlog.MaskKey(appKey), method, path, signutil.SHA256Hex(bodyBytes),
|
||||
openlog.MaskSign(sign), openlog.MaskSign(expected))
|
||||
response.Unauthorized(c, "签名校验失败")
|
||||
c.Abort()
|
||||
@@ -189,16 +185,15 @@ func GetMerchantID(c *gin.Context) uint {
|
||||
// 与上游 BuildSignString 风格一致;body 以 SHA256 摘要参与签名(避免大 body 与特殊字符问题)。
|
||||
// 参与签名的参数固定顺序为:app_key, body_sha256, method, nonce, path, timestamp。
|
||||
func BuildOpenV1Sign(secret, appKey, timestamp, nonce, method, path string, body []byte) string {
|
||||
bodyHash := sha256.Sum256(body)
|
||||
content := strings.Join([]string{
|
||||
"app_key=" + appKey,
|
||||
"body_sha256=" + hex.EncodeToString(bodyHash[:]),
|
||||
"body_sha256=" + signutil.SHA256Hex(body),
|
||||
"method=" + strings.ToUpper(method),
|
||||
"nonce=" + nonce,
|
||||
"path=" + path,
|
||||
"timestamp=" + timestamp,
|
||||
}, "&")
|
||||
return hmacSHA256Hex(secret, content)
|
||||
return signutil.HMACSHA256Hex(secret, content)
|
||||
}
|
||||
|
||||
// BuildSignString 保留旧接口的字典序签名算法,供上游发货兼容客户端和测试使用。
|
||||
@@ -215,18 +210,5 @@ func BuildSignString(apiKey, timestamp, nonce, method, path, body string) string
|
||||
|
||||
// BuildOpenSign 供旧发货系统兼容使用。
|
||||
func BuildOpenSign(apiKey, apiSecret, timestamp, nonce, method, path, body string) string {
|
||||
return hmacSHA256Hex(apiSecret, BuildSignString(apiKey, timestamp, nonce, method, path, body))
|
||||
}
|
||||
|
||||
func hmacSHA256Hex(secret, content string) string {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, _ = mac.Write([]byte(content))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func abs64(value int64) int64 {
|
||||
if value < 0 {
|
||||
return -value
|
||||
}
|
||||
return value
|
||||
return signutil.HMACSHA256Hex(apiSecret, BuildSignString(apiKey, timestamp, nonce, method, path, body))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user