77 lines
2.3 KiB
Go
77 lines
2.3 KiB
Go
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()
|
||
}
|
||
}
|