130 lines
3.8 KiB
Go
130 lines
3.8 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"affiliate_dash/internal/pkg/openlog"
|
|
"affiliate_dash/internal/pkg/response"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SourceOpenAuthConfig 是原上游发货接口鉴权配置,保持 X-Api-Key 兼容。
|
|
type SourceOpenAuthConfig struct {
|
|
APIKey string
|
|
APISecret string
|
|
SkewSeconds int64
|
|
Debug bool
|
|
}
|
|
|
|
type sourceNonceStore struct {
|
|
mu sync.Mutex
|
|
data map[string]int64
|
|
}
|
|
|
|
func newSourceNonceStore() *sourceNonceStore {
|
|
return &sourceNonceStore{data: make(map[string]int64)}
|
|
}
|
|
|
|
func (s *sourceNonceStore) seen(nonce string, now, ttl int64) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for key, expiresAt := range s.data {
|
|
if expiresAt < now {
|
|
delete(s.data, key)
|
|
}
|
|
}
|
|
if expiresAt, ok := s.data[nonce]; ok && expiresAt >= now {
|
|
return true
|
|
}
|
|
s.data[nonce] = now + ttl
|
|
return false
|
|
}
|
|
|
|
// SourceOpenAuth 保持现有上游对接签名算法不变:X-Api-Key + 字典序 HMAC。
|
|
func SourceOpenAuth(cfg SourceOpenAuthConfig) gin.HandlerFunc {
|
|
if cfg.SkewSeconds <= 0 {
|
|
cfg.SkewSeconds = 300
|
|
}
|
|
store := newSourceNonceStore()
|
|
return func(c *gin.Context) {
|
|
reqID := openlog.EnsureReqID(c)
|
|
side, action := openlog.ScopeFromPath(openlog.SideSource, c.Request.Method, c.Request.URL.Path)
|
|
openlog.SetScope(c, side, action)
|
|
c.Set(openlog.CtxDebug, cfg.Debug)
|
|
c.Set(openlog.CtxStart, time.Now())
|
|
c.Header("X-Request-Id", reqID)
|
|
|
|
if cfg.APIKey == "" || cfg.APISecret == "" {
|
|
openlog.Warn(c, "source_open_auth uninitialized")
|
|
response.ServerError(c, "服务端未配置 OPEN_API_KEY / OPEN_API_SECRET")
|
|
c.Abort()
|
|
return
|
|
}
|
|
apiKey := c.GetHeader("X-Api-Key")
|
|
timestamp := c.GetHeader("X-Timestamp")
|
|
nonce := c.GetHeader("X-Nonce")
|
|
sign := c.GetHeader("X-Sign")
|
|
if apiKey == "" || timestamp == "" || nonce == "" || sign == "" {
|
|
openlog.Warn(c, "source_open_auth missing_headers")
|
|
response.Unauthorized(c, "缺少鉴权头:需要 X-Api-Key、X-Timestamp、X-Nonce、X-Sign")
|
|
c.Abort()
|
|
return
|
|
}
|
|
if apiKey != cfg.APIKey {
|
|
openlog.Warn(c, "source_open_auth invalid_key api_key=%s", openlog.MaskKey(apiKey))
|
|
response.Unauthorized(c, "无效的 API Key")
|
|
c.Abort()
|
|
return
|
|
}
|
|
if len(nonce) < 8 || len(nonce) > 64 {
|
|
openlog.Warn(c, "source_open_auth bad_nonce_len len=%d", len(nonce))
|
|
response.Unauthorized(c, "X-Nonce 长度需在 8~64 之间")
|
|
c.Abort()
|
|
return
|
|
}
|
|
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
|
if err != nil || abs64(time.Now().Unix()-ts) > cfg.SkewSeconds {
|
|
openlog.Warn(c, "source_open_auth expired ts=%s skew=%d", timestamp, cfg.SkewSeconds)
|
|
response.Unauthorized(c, "请求已过期或 X-Timestamp 格式错误")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
openlog.Warn(c, "source_open_auth read_body_fail err=%v", err)
|
|
response.BadRequest(c, "读取请求体失败")
|
|
c.Abort()
|
|
return
|
|
}
|
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
|
|
if store.seen(apiKey+":"+nonce, time.Now().Unix(), cfg.SkewSeconds) {
|
|
openlog.Warn(c, "source_open_auth nonce_replay nonce=%s", nonce)
|
|
response.Unauthorized(c, "重复的 X-Nonce(请勿重放请求)")
|
|
c.Abort()
|
|
return
|
|
}
|
|
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)) {
|
|
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))
|
|
response.Unauthorized(c, "签名校验失败")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
openlog.Info(c, "source_open_auth ok method=%s path=%s body_size=%d",
|
|
c.Request.Method, c.Request.URL.Path, len(bodyBytes))
|
|
c.Next()
|
|
}
|
|
}
|