package middleware import ( "bytes" "crypto/hmac" "io" "strconv" "strings" "time" "affiliate_dash/internal/model" "affiliate_dash/internal/pkg/openlog" "affiliate_dash/internal/pkg/response" "github.com/gin-gonic/gin" "gorm.io/gorm" "gorm.io/gorm/clause" ) // SourceOpenAuthConfig 是原上游发货接口鉴权配置,保持 X-Api-Key 兼容。 type SourceOpenAuthConfig struct { DB *gorm.DB APIKey string APISecret string SkewSeconds int64 Debug bool } // SourceOpenAuth 保持现有上游对接签名算法不变:X-Api-Key + 字典序 HMAC。 func SourceOpenAuth(cfg SourceOpenAuthConfig) gin.HandlerFunc { if cfg.SkewSeconds <= 0 { cfg.SkewSeconds = 300 } 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.DB == nil { openlog.Warn(c, "source_open_auth uninitialized_db") response.ServerError(c, "源头开放接口认证服务未初始化") c.Abort() return } 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)) 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)) { 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 } now := time.Now() _ = cfg.DB.Where("expires_at < ?", now).Delete(&model.SourceAPINonce{}).Error created := cfg.DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&model.SourceAPINonce{ APIKey: apiKey, Nonce: nonce, ExpiresAt: now.Add(time.Duration(cfg.SkewSeconds) * time.Second), }) if created.Error != nil { openlog.Warn(c, "source_open_auth nonce_db_fail err=%v", created.Error) response.ServerError(c, "记录请求 nonce 失败") c.Abort() return } if created.RowsAffected == 0 { openlog.Warn(c, "source_open_auth nonce_replay nonce=%s", nonce) response.Unauthorized(c, "重复的 X-Nonce(请勿重放请求)") 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() } }