开放接口增加详细调试日志,并支持控制台与文件双写
- OPEN_API_DEBUG 记录鉴权、查单、推送业务字段与 req_id - LOG_FILE 默认 logs/app.log,标准日志/Gin/GORM 同时输出控制台与文件
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"affiliate_dash/internal/pkg/openlog"
|
||||
"affiliate_dash/internal/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -23,6 +24,8 @@ type OpenAuthConfig struct {
|
||||
APISecret string
|
||||
// 允许的时间偏差(秒),默认 300
|
||||
SkewSeconds int64
|
||||
// Debug 详细日志
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// nonce 防重放(进程内,重启清空;生产可换 Redis)
|
||||
@@ -56,10 +59,7 @@ func (s *nonceStore) seen(nonce string, now, ttl int64) bool {
|
||||
//
|
||||
// api_key, body, method, nonce, path, timestamp
|
||||
//
|
||||
// 按 key 字典序排序后拼接:
|
||||
//
|
||||
// k1=v1&k2=v2&...
|
||||
//
|
||||
// 按 key 字典序排序后拼接 k1=v1&k2=v2&...
|
||||
// method 大写;path 为 URL.Path(不含 query);GET 时 body 为空串。
|
||||
// sign = hex(hmac_sha256(apiSecret, stringToSign)),小写十六进制。
|
||||
func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc {
|
||||
@@ -69,7 +69,13 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc {
|
||||
store := newNonceStore()
|
||||
|
||||
return func(c *gin.Context) {
|
||||
reqID := openlog.EnsureReqID(c)
|
||||
c.Set(openlog.CtxDebug, cfg.Debug)
|
||||
c.Set(openlog.CtxStart, time.Now())
|
||||
c.Header("X-Request-Id", reqID)
|
||||
|
||||
if cfg.APIKey == "" || cfg.APISecret == "" {
|
||||
openlog.Warn(c, "auth_fail reason=server_not_configured")
|
||||
response.ServerError(c, "服务端未配置 OPEN_API_KEY / OPEN_API_SECRET")
|
||||
c.Abort()
|
||||
return
|
||||
@@ -79,18 +85,43 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc {
|
||||
timestamp := c.GetHeader("X-Timestamp")
|
||||
nonce := c.GetHeader("X-Nonce")
|
||||
sign := c.GetHeader("X-Sign")
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
openlog.Warn(c, "auth_fail reason=read_body_error err=%v ip=%s", err, clientIP)
|
||||
response.BadRequest(c, "读取请求体失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
body := string(bodyBytes)
|
||||
c.Set(openlog.CtxBody, body)
|
||||
|
||||
method := strings.ToUpper(c.Request.Method)
|
||||
path := c.Request.URL.Path
|
||||
|
||||
openlog.Info(c, "request in method=%s path=%s ip=%s key=%s ts=%s nonce=%s sign=%s body=%q body_len=%d",
|
||||
method, path, clientIP,
|
||||
openlog.MaskKey(apiKey), timestamp, nonce, openlog.MaskSign(sign),
|
||||
openlog.Truncate(body, 500), len(bodyBytes),
|
||||
)
|
||||
|
||||
if apiKey == "" || timestamp == "" || nonce == "" || sign == "" {
|
||||
openlog.Warn(c, "auth_fail reason=missing_headers key=%s ts=%s nonce=%s has_sign=%v",
|
||||
openlog.MaskKey(apiKey), timestamp, nonce, sign != "")
|
||||
response.Unauthorized(c, "缺少鉴权头:需要 X-Api-Key、X-Timestamp、X-Nonce、X-Sign")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if apiKey != cfg.APIKey {
|
||||
openlog.Warn(c, "auth_fail reason=invalid_api_key key=%s", openlog.MaskKey(apiKey))
|
||||
response.Unauthorized(c, "无效的 API Key")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if len(nonce) < 8 || len(nonce) > 64 {
|
||||
openlog.Warn(c, "auth_fail reason=bad_nonce_len len=%d", len(nonce))
|
||||
response.Unauthorized(c, "X-Nonce 长度需在 8~64 之间")
|
||||
c.Abort()
|
||||
return
|
||||
@@ -98,44 +129,45 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc {
|
||||
|
||||
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
||||
if err != nil {
|
||||
openlog.Warn(c, "auth_fail reason=bad_timestamp raw=%s", timestamp)
|
||||
response.Unauthorized(c, "X-Timestamp 格式错误,需为 Unix 秒级时间戳")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
if abs64(now-ts) > cfg.SkewSeconds {
|
||||
skew := now - ts
|
||||
if abs64(skew) > cfg.SkewSeconds {
|
||||
openlog.Warn(c, "auth_fail reason=timestamp_skew server_now=%d ts=%d skew=%ds limit=%ds",
|
||||
now, ts, skew, cfg.SkewSeconds)
|
||||
response.Unauthorized(c, "请求已过期或时间偏差过大")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if store.seen(apiKey+":"+nonce, now, cfg.SkewSeconds) {
|
||||
openlog.Warn(c, "auth_fail reason=replay_nonce nonce=%s", nonce)
|
||||
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)) {
|
||||
// 调试时输出待签名串(不含 secret),便于源头对齐
|
||||
openlog.Warn(c, "auth_fail reason=sign_mismatch sign=%s expected_prefix=%s string_to_sign=%q",
|
||||
openlog.MaskSign(sign), openlog.MaskSign(expected), openlog.Truncate(stringToSign, 800))
|
||||
response.Unauthorized(c, "签名校验失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(openlog.CtxAPIKey, apiKey)
|
||||
openlog.Info(c, "auth_ok skew=%ds string_to_sign_len=%d", skew, len(stringToSign))
|
||||
c.Next()
|
||||
|
||||
// 请求结束后补一条总耗时(handler 里会打业务结果)
|
||||
openlog.Info(c, "request done status=%d cost=%s", c.Writer.Status(), openlog.Elapsed(c).Round(time.Microsecond))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user