This commit is contained in:
toom1996
2026-08-30 11:23:17 +08:00
parent 9c3403a903
commit ec1ee702bd
6 changed files with 540 additions and 0 deletions

View File

@ -0,0 +1,76 @@
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()
}
}