package middleware import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "sort" "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 签名 // // 待签名参数(value 原样,不做 URL encode): // // api_key, body, method, nonce, path, timestamp // // 按 key 字典序排序后拼接: // // k1=v1&k2=v2&... // // method 大写;path 为 URL.Path(不含 query);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 } 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 := BuildSignString(apiKey, timestamp, nonce, method, path, body) expected := hmacSHA256Hex(cfg.APISecret, stringToSign) if !hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected)) { response.Unauthorized(c, "签名校验失败") c.Abort() return } c.Next() } } // BuildSignString 生成待签名字符串:参数字典序 + & 拼接 func BuildSignString(apiKey, timestamp, nonce, method, path, body string) string { params := map[string]string{ "api_key": apiKey, "body": body, "method": strings.ToUpper(method), "nonce": nonce, "path": path, "timestamp": timestamp, } keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, k := range keys { parts = append(parts, k+"="+params[k]) } return strings.Join(parts, "&") } 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 { return hmacSHA256Hex(apiSecret, BuildSignString(apiKey, timestamp, nonce, method, path, body)) }