package middleware import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "net/http" "strconv" "strings" "time" "fashionapi/internal/config" "fashionapi/internal/pkg/response" "github.com/gin-gonic/gin" ) const ( hdrSign = "X-Sign" hdrTs = "X-Sign-Ts" hdrNonce = "X-Sign-Nonce" ) // ClientSign 校验「公开接口」请求是否由前端 JS 签名(防简单爬虫/批量抓取的一层)。 // // 校验内容:HMAC-SHA256(secret, METHOD + "\n" + Path + "\n" + RawQuery + "\n" + ts + "\n" + nonce) // 并要求 ts 在 ±TTL 秒以内(防重放)。 // // 安全说明(务必知悉): // - 这是「提高成本」而非「加密」:secret 必须出现在前端 bundle 才能签名,因此本质上对浏览器可见, // 有决心的爬虫可反编译 JS 复刻签名逻辑。它用于拖慢 casual 爬虫,不能作为唯一防线。 // - 真正有效的组合是:本中间件(识别「大概率真前端」)+ 按 IP 限流(兜住总量)。 // - Enabled=false 或 Secret 为空时本中间件为 noop(不拦截),便于灰度上线与回滚。 func ClientSign(cfg config.ClientSignConfig) gin.HandlerFunc { if !cfg.Enabled || cfg.Secret == "" { return func(c *gin.Context) { c.Next() } } ttl := cfg.TTLSeconds if ttl <= 0 { ttl = 30 } secret := []byte(cfg.Secret) return func(c *gin.Context) { sig := c.GetHeader(hdrSign) ts := c.GetHeader(hdrTs) nonce := c.GetHeader(hdrNonce) if sig == "" || ts == "" || nonce == "" { response.AbortError(c, http.StatusUnauthorized, "missing client signature") return } ti, err := strconv.ParseInt(ts, 10, 64) if err != nil { response.AbortError(c, http.StatusUnauthorized, "invalid signature timestamp") return } now := time.Now().Unix() if diff := now - ti; diff < -int64(ttl) || diff > int64(ttl) { response.AbortError(c, http.StatusUnauthorized, "expired signature") return } mac := hmac.New(sha256.New, secret) mac.Write([]byte(strings.Join([]string{ c.Request.Method, c.Request.URL.Path, c.Request.URL.RawQuery, ts, nonce, }, "\n"))) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expected), []byte(sig)) { response.AbortError(c, http.StatusUnauthorized, "bad client signature") return } c.Next() } }