package middleware import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "strconv" "strings" "sync" "time" "affiliate_dash/internal/pkg/response" "github.com/gin-gonic/gin" ) // OpenAuthConfig 开放接口鉴权配置 type OpenAuthConfig struct { APIKey string APISecret string // 允许的时间偏差(秒),默认 300 SkewSeconds int64 } // nonce 防重放(进程内,重启清空;生产可换 Redis) type nonceStore struct { mu sync.Mutex data map[string]int64 // nonce -> expire unix } func newNonceStore() *nonceStore { return &nonceStore{data: make(map[string]int64)} } func (s *nonceStore) seen(nonce string, now, ttl int64) bool { s.mu.Lock() defer s.mu.Unlock() // 清理过期 for k, exp := range s.data { if exp < now { delete(s.data, k) } } if exp, ok := s.data[nonce]; ok && exp >= now { return true } s.data[nonce] = now + ttl return false } // OpenAuth 校验 X-Api-Key + 时间戳 + nonce + HMAC-SHA256 签名 // // 待签名字符串(UTF-8,\n 换行): // // {apiKey}\n{timestamp}\n{nonce}\n{METHOD}\n{path}\n{body} // // path 为 URL.Path(不含 query),METHOD 大写;GET 时 body 为空字符串。 // sign = hex(hmac_sha256(apiSecret, stringToSign)),小写十六进制。 func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc { if cfg.SkewSeconds <= 0 { cfg.SkewSeconds = 300 } store := newNonceStore() return func(c *gin.Context) { if cfg.APIKey == "" || cfg.APISecret == "" { 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 == "" { response.Unauthorized(c, "缺少鉴权头:需要 X-Api-Key、X-Timestamp、X-Nonce、X-Sign") c.Abort() return } if apiKey != cfg.APIKey { response.Unauthorized(c, "无效的 API Key") c.Abort() return } if len(nonce) < 8 || len(nonce) > 64 { response.Unauthorized(c, "X-Nonce 长度需在 8~64 之间") c.Abort() return } ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { response.Unauthorized(c, "X-Timestamp 格式错误,需为 Unix 秒级时间戳") c.Abort() return } now := time.Now().Unix() if abs64(now-ts) > cfg.SkewSeconds { response.Unauthorized(c, "请求已过期或时间偏差过大") c.Abort() return } // 防重放:同一 nonce 在时间窗口内只能用一次 if store.seen(apiKey+":"+nonce, now, cfg.SkewSeconds) { response.Unauthorized(c, "重复的 X-Nonce(请勿重放请求)") c.Abort() return } bodyBytes, err := io.ReadAll(c.Request.Body) if err != nil { response.BadRequest(c, "读取请求体失败") c.Abort() return } c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) method := strings.ToUpper(c.Request.Method) path := c.Request.URL.Path body := string(bodyBytes) stringToSign := strings.Join([]string{ apiKey, timestamp, nonce, method, path, body, }, "\n") expected := hmacSHA256Hex(cfg.APISecret, stringToSign) if !hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected)) { response.Unauthorized(c, "签名校验失败") c.Abort() return } c.Next() } } 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(v int64) int64 { if v < 0 { return -v } return v } // BuildOpenSign 供测试或内部生成签名(与 OpenAuth 规则一致) func BuildOpenSign(apiKey, apiSecret, timestamp, nonce, method, path, body string) string { stringToSign := strings.Join([]string{ apiKey, timestamp, nonce, strings.ToUpper(method), path, body, }, "\n") return hmacSHA256Hex(apiSecret, stringToSign) }