package middleware import ( "io" "net/http" "strings" "fashionapi/internal/pkg/hmac" "fashionapi/internal/repository" "github.com/gin-gonic/gin" ) // IngestAuthConfig 爬虫上报接口验签所需依赖。 type IngestAuthConfig struct { Secret string TTL int Nonces repository.IngestRepository // 复用的 nonce 表(与任务队列表同库) } // IngestAuth 校验爬虫上报的 HMAC 签名 + nonce 防重放。 // // 流程:读原始 body → 校验 X-Signature / X-Timestamp / X-Nonce(签名 + ±TTL 时间戳) // → 用 nonce 表 ReserveNonce 去重(已存在即重放,返回 409)→ 通过则放行。 // 失败一律返回 401(签名/时间戳)或 409(重放),不泄露具体原因。 func IngestAuth(cfg IngestAuthConfig) gin.HandlerFunc { return func(c *gin.Context) { body, err := io.ReadAll(c.Request.Body) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "read body failed"}) return } // 还原 body,供后续 handler 再读 c.Request.Body = io.NopCloser(strings.NewReader(string(body))) sig := c.GetHeader("X-Signature") ts := c.GetHeader("X-Timestamp") nonce := c.GetHeader("X-Nonce") if !hmac.Verify(cfg.Secret, string(body), sig, ts, nonce, cfg.TTL) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"}) return } ok, err := cfg.Nonces.ReserveNonce(c.Request.Context(), nonce) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "nonce store error"}) return } if !ok { c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": "replay detected"}) return } c.Next() } }