update
This commit is contained in:
56
internal/middleware/ingest_auth.go
Normal file
56
internal/middleware/ingest_auth.go
Normal file
@ -0,0 +1,56 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user