181 lines
4.3 KiB
Go
181 lines
4.3 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"affiliate_dash/internal/pkg/response"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// OpenAuthConfig 开放接口鉴权配置
|
|
type OpenAuthConfig struct {
|
|
APIKey string
|
|
APISecret string
|
|
// 允许的时间偏差(秒),默认 300
|
|
SkewSeconds int64
|
|
}
|
|
|
|
// nonce 防重放(进程内,重启清空;生产可换 Redis)
|
|
type nonceStore struct {
|
|
mu sync.Mutex
|
|
data map[string]int64 // nonce -> expire unix
|
|
}
|
|
|
|
func newNonceStore() *nonceStore {
|
|
return &nonceStore{data: make(map[string]int64)}
|
|
}
|
|
|
|
func (s *nonceStore) seen(nonce string, now, ttl int64) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for k, exp := range s.data {
|
|
if exp < now {
|
|
delete(s.data, k)
|
|
}
|
|
}
|
|
if exp, ok := s.data[nonce]; ok && exp >= now {
|
|
return true
|
|
}
|
|
s.data[nonce] = now + ttl
|
|
return false
|
|
}
|
|
|
|
// OpenAuth 校验 X-Api-Key + 时间戳 + nonce + HMAC-SHA256 签名
|
|
//
|
|
// 待签名参数(value 原样,不做 URL encode):
|
|
//
|
|
// api_key, body, method, nonce, path, timestamp
|
|
//
|
|
// 按 key 字典序排序后拼接:
|
|
//
|
|
// k1=v1&k2=v2&...
|
|
//
|
|
// method 大写;path 为 URL.Path(不含 query);GET 时 body 为空串。
|
|
// sign = hex(hmac_sha256(apiSecret, stringToSign)),小写十六进制。
|
|
func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc {
|
|
if cfg.SkewSeconds <= 0 {
|
|
cfg.SkewSeconds = 300
|
|
}
|
|
store := newNonceStore()
|
|
|
|
return func(c *gin.Context) {
|
|
if cfg.APIKey == "" || cfg.APISecret == "" {
|
|
response.ServerError(c, "服务端未配置 OPEN_API_KEY / OPEN_API_SECRET")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
apiKey := c.GetHeader("X-Api-Key")
|
|
timestamp := c.GetHeader("X-Timestamp")
|
|
nonce := c.GetHeader("X-Nonce")
|
|
sign := c.GetHeader("X-Sign")
|
|
|
|
if apiKey == "" || timestamp == "" || nonce == "" || sign == "" {
|
|
response.Unauthorized(c, "缺少鉴权头:需要 X-Api-Key、X-Timestamp、X-Nonce、X-Sign")
|
|
c.Abort()
|
|
return
|
|
}
|
|
if apiKey != cfg.APIKey {
|
|
response.Unauthorized(c, "无效的 API Key")
|
|
c.Abort()
|
|
return
|
|
}
|
|
if len(nonce) < 8 || len(nonce) > 64 {
|
|
response.Unauthorized(c, "X-Nonce 长度需在 8~64 之间")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
|
if err != nil {
|
|
response.Unauthorized(c, "X-Timestamp 格式错误,需为 Unix 秒级时间戳")
|
|
c.Abort()
|
|
return
|
|
}
|
|
now := time.Now().Unix()
|
|
if abs64(now-ts) > cfg.SkewSeconds {
|
|
response.Unauthorized(c, "请求已过期或时间偏差过大")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
if store.seen(apiKey+":"+nonce, now, cfg.SkewSeconds) {
|
|
response.Unauthorized(c, "重复的 X-Nonce(请勿重放请求)")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
response.BadRequest(c, "读取请求体失败")
|
|
c.Abort()
|
|
return
|
|
}
|
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
|
|
method := strings.ToUpper(c.Request.Method)
|
|
path := c.Request.URL.Path
|
|
body := string(bodyBytes)
|
|
|
|
stringToSign := BuildSignString(apiKey, timestamp, nonce, method, path, body)
|
|
expected := hmacSHA256Hex(cfg.APISecret, stringToSign)
|
|
if !hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected)) {
|
|
response.Unauthorized(c, "签名校验失败")
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// BuildSignString 生成待签名字符串:参数字典序 + & 拼接
|
|
func BuildSignString(apiKey, timestamp, nonce, method, path, body string) string {
|
|
params := map[string]string{
|
|
"api_key": apiKey,
|
|
"body": body,
|
|
"method": strings.ToUpper(method),
|
|
"nonce": nonce,
|
|
"path": path,
|
|
"timestamp": timestamp,
|
|
}
|
|
keys := make([]string, 0, len(params))
|
|
for k := range params {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
parts := make([]string, 0, len(keys))
|
|
for _, k := range keys {
|
|
parts = append(parts, k+"="+params[k])
|
|
}
|
|
return strings.Join(parts, "&")
|
|
}
|
|
|
|
func hmacSHA256Hex(secret, content string) string {
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
_, _ = mac.Write([]byte(content))
|
|
return hex.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func abs64(v int64) int64 {
|
|
if v < 0 {
|
|
return -v
|
|
}
|
|
return v
|
|
}
|
|
|
|
// BuildOpenSign 供测试或内部生成签名(与 OpenAuth 规则一致)
|
|
func BuildOpenSign(apiKey, apiSecret, timestamp, nonce, method, path, body string) string {
|
|
return hmacSHA256Hex(apiSecret, BuildSignString(apiKey, timestamp, nonce, method, path, body))
|
|
}
|