Files
backend_v2/internal/middleware/clientsign.go
toom1996 ec1ee702bd update
2026-08-30 11:23:17 +08:00

77 lines
2.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}