57 lines
1.6 KiB
Go
57 lines
1.6 KiB
Go
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()
|
||
}
|
||
}
|