61 lines
2.3 KiB
Go
61 lines
2.3 KiB
Go
// Package hmac 提供爬虫 → 后台 ingest 接口的 HMAC-SHA256 请求签名与校验。
|
||
//
|
||
// 设计目标(与公开接口「前端 JS 签名」区分):
|
||
// - 这里面向「服务端到服务端」的爬虫上报,密钥绝不下发到任何前端 bundle,
|
||
// 只存在于爬虫配置与后台 INGEST_SECRET 环境变量,安全性高得多。
|
||
// - 通过 X-Signature / X-Timestamp / X-Nonce 三头防篡改 + 防重放:
|
||
// - X-Timestamp 容忍窗口(默认 ±5 分钟)挡掉过期请求;
|
||
// - X-Nonce 一次性随机串,由后台入库去重挡掉重放(见 repository.IngestRepository 的 nonce 表)。
|
||
//
|
||
// 签名串拼接:HMAC_SHA256(secret, timestamp + "." + nonce + "." + bodyRaw)
|
||
// bodyRaw 是请求体的原始字节(未编码),保证签名与服务端收到的字节严格一致。
|
||
package hmac
|
||
|
||
import (
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// DefaultTTL 签名时间戳默认容忍窗口(秒)。
|
||
const DefaultTTL = 300
|
||
|
||
// Sign 生成签名:对 timestamp.nonce.body 做 HMAC-SHA256,返回十六进制串。
|
||
func Sign(secret, timestamp, nonce, body string) string {
|
||
mac := hmac.New(sha256.New, []byte(secret))
|
||
mac.Write([]byte(timestamp))
|
||
mac.Write([]byte("."))
|
||
mac.Write([]byte(nonce))
|
||
mac.Write([]byte("."))
|
||
mac.Write([]byte(body))
|
||
return hex.EncodeToString(mac.Sum(nil))
|
||
}
|
||
|
||
// Verify 校验请求签名,同时检查时间戳窗口。
|
||
//
|
||
// 返回 (ok, error):ok=false 表示签名或时间戳不通过;error 仅用于内部异常(理论上不会返回)。
|
||
// 注:nonce 防重放不在此处判断,交由调用方(中间件 / 仓储)查库,
|
||
// 因为 nonce 是否重复依赖持久化状态,且失败时应返回 409 而非 401。
|
||
func Verify(secret, body, sigHeader, tsHeader, nonceHeader string, ttl int) bool {
|
||
if sigHeader == "" || tsHeader == "" || nonceHeader == "" {
|
||
return false
|
||
}
|
||
if ttl <= 0 {
|
||
ttl = DefaultTTL
|
||
}
|
||
ts, err := strconv.ParseInt(tsHeader, 10, 64)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
now := time.Now().Unix()
|
||
if diff := now - ts; diff > int64(ttl) || diff < -int64(ttl) {
|
||
return false
|
||
}
|
||
expected := Sign(secret, tsHeader, nonceHeader, body)
|
||
// 定长比较防时序侧信道
|
||
return hmac.Equal([]byte(expected), []byte(strings.TrimSpace(sigHeader)))
|
||
}
|