dual check v0.1
This commit is contained in:
1 parent
78bad1fa3d
commit
c137299016
36 files changed
+3727
-81
No files matched your search
@@ -0,0 +1,60 @@
|
||||
// Package middleware 提供 HTTP 中间件
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CORS 跨域中间件
|
||||
// - 白名单 origin 精确匹配(生产用)
|
||||
// - 同时自动放行 http(s)://localhost:* 所有端口(开发期 Flutter Web/React Vite 随机端口)
|
||||
// - 不在范围内的请求不设置 Allow-Origin 头,浏览器会自行阻止
|
||||
func CORS(allowedOrigins []string) gin.HandlerFunc {
|
||||
originSet := make(map[string]bool, len(allowedOrigins))
|
||||
for _, o := range allowedOrigins {
|
||||
originSet[strings.TrimRight(o, "/")] = true
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
origin := strings.TrimRight(c.GetHeader("Origin"), "/")
|
||||
if origin == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 是否允许:白名单精确匹配 或 localhost 通配
|
||||
allowed := originSet[origin] || isLocalhostOrigin(origin)
|
||||
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
if allowed {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID")
|
||||
c.Writer.Header().Set("Access-Control-Expose-Headers", "X-Request-ID")
|
||||
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if allowed {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
c.Writer.Header().Set("Vary", "Origin")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// isLocalhostOrigin 匹配 http://localhost:* 和 http://127.0.0.1:*
|
||||
// 开发期 Flutter Web / Vite / CRA 等工具常用随机端口
|
||||
func isLocalhostOrigin(origin string) bool {
|
||||
return strings.HasPrefix(origin, "http://localhost:") ||
|
||||
strings.HasPrefix(origin, "http://127.0.0.1:") ||
|
||||
strings.HasPrefix(origin, "http://[::1]:")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Logger 基于 zap 的请求日志中间件
|
||||
// 只记录 method/path/status/latency/request_id,**不记录请求体**(避免泄露用户输入)
|
||||
func Logger(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
latency := time.Since(start)
|
||||
|
||||
logger.Info("HTTP",
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
zap.Int("size", c.Writer.Size()),
|
||||
zap.String("remote_ip", c.ClientIP()),
|
||||
zap.String("user_agent", c.Request.UserAgent()),
|
||||
zap.String("request_id", c.GetString("request_id")),
|
||||
zap.Duration("latency", latency),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ulule/limiter/v3"
|
||||
"github.com/ulule/limiter/v3/drivers/store/memory"
|
||||
)
|
||||
|
||||
// RateLimitConfig 限流配置
|
||||
type RateLimitConfig struct {
|
||||
GlobalRPS float64
|
||||
PerIPRPS float64
|
||||
PerIPBurst int
|
||||
}
|
||||
|
||||
// RateLimiter 双维度限流:全局令牌桶 + 单 IP 令牌桶
|
||||
// 开发/测试环境可用内存存储,生产环境建议换 Redis 存储
|
||||
func RateLimiter(cfg RateLimitConfig) gin.HandlerFunc {
|
||||
store := memory.NewStore()
|
||||
|
||||
// 全局限流
|
||||
globalRate := limiter.Rate{
|
||||
Period: time.Second,
|
||||
Limit: int64(cfg.GlobalRPS),
|
||||
}
|
||||
globalLimiter := limiter.New(store, globalRate)
|
||||
|
||||
// 单 IP 限流
|
||||
perIPRate := limiter.Rate{
|
||||
Period: time.Second,
|
||||
Limit: int64(cfg.PerIPRPS),
|
||||
}
|
||||
perIPLimiter := limiter.New(store, perIPRate)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
// 1. 检查全局限流
|
||||
globalCtx, err := globalLimiter.Get(c.Request.Context(), "global")
|
||||
if err != nil || globalCtx.Reached {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"code": 4290,
|
||||
"message": "server busy, please retry later",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 检查单 IP 限流
|
||||
ip := c.ClientIP()
|
||||
ipCtx, err := perIPLimiter.Get(c.Request.Context(), "ip:"+ip)
|
||||
if err != nil || ipCtx.Reached {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"code": 4291,
|
||||
"message": "too many requests from this IP",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Recover 捕获 panic 并返回 500
|
||||
func Recover(logger *zap.Logger) gin.HandlerFunc {
|
||||
return gin.CustomRecovery(func(c *gin.Context, err interface{}) {
|
||||
logger.Error("panic recovered",
|
||||
zap.Any("error", err),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("request_id", c.GetString("request_id")),
|
||||
)
|
||||
c.JSON(500, gin.H{
|
||||
"code": 5000,
|
||||
"message": "internal server error",
|
||||
})
|
||||
c.Abort()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RequestID 为每个请求生成唯一 ID,放入 context 和响应头
|
||||
func RequestID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 优先使用客户端传来的 X-Request-ID
|
||||
id := c.GetHeader("X-Request-ID")
|
||||
if id == "" {
|
||||
b := make([]byte, 12)
|
||||
if _, err := rand.Read(b); err == nil {
|
||||
id = hex.EncodeToString(b)
|
||||
} else {
|
||||
id = "unknown"
|
||||
}
|
||||
}
|
||||
c.Set("request_id", id)
|
||||
c.Header("X-Request-ID", id)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user