// Filename: internal/middleware/logging.go package middleware import ( "bytes" "io" "regexp" "strings" "time" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) const ( RedactedBodyKey = "redactedBody" RedactedAuthHeaderKey = "redactedAuthHeader" RedactedValue = `"[REDACTED]"` ) // 预编译正则表达式(全局变量,提升性能) var ( // JSON 敏感字段脱敏 jsonSensitiveKeys = regexp.MustCompile(`("(?i:api_key|apikey|token|password|secret|authorization|key|keys|auth)"\s*:\s*)"[^"]*"`) // Bearer Token 脱敏 bearerTokenPattern = regexp.MustCompile(`^(Bearer\s+)\S+$`) // URL 中的 key 参数脱敏 queryKeyPattern = regexp.MustCompile(`([?&](?i:key|token|apikey)=)[^&\s]+`) ) // RedactionMiddleware 请求数据脱敏中间件 func RedactionMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 1. 脱敏请求体 if shouldRedactBody(c) { redactRequestBody(c) } // 2. 脱敏认证头 redactAuthHeader(c) // 3. 脱敏 URL 查询参数 redactQueryParams(c) c.Next() } } // shouldRedactBody 判断是否需要脱敏请求体 func shouldRedactBody(c *gin.Context) bool { method := c.Request.Method contentType := c.GetHeader("Content-Type") // 只处理包含 JSON 的 POST/PUT/PATCH/DELETE 请求 return (method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE") && strings.Contains(contentType, "application/json") } // redactRequestBody 脱敏请求体 func redactRequestBody(c *gin.Context) { bodyBytes, err := io.ReadAll(c.Request.Body) if err != nil { return } // 恢复请求体供后续使用 c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // 脱敏敏感字段 bodyString := string(bodyBytes) redactedBody := jsonSensitiveKeys.ReplaceAllString(bodyString, `$1`+RedactedValue) c.Set(RedactedBodyKey, redactedBody) } // redactAuthHeader 脱敏认证头 func redactAuthHeader(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { return } if bearerTokenPattern.MatchString(authHeader) { redacted := bearerTokenPattern.ReplaceAllString(authHeader, `${1}[REDACTED]`) c.Set(RedactedAuthHeaderKey, redacted) } else { // 对于非 Bearer 的 token,全部脱敏 c.Set(RedactedAuthHeaderKey, "[REDACTED]") } // 同时处理其他敏感 Header sensitiveHeaders := []string{"X-Api-Key", "X-Goog-Api-Key", "Api-Key"} for _, header := range sensitiveHeaders { if value := c.GetHeader(header); value != "" { c.Set("redacted_"+header, "[REDACTED]") } } } // redactQueryParams 脱敏 URL 查询参数 func redactQueryParams(c *gin.Context) { rawQuery := c.Request.URL.RawQuery if rawQuery == "" { return } redacted := queryKeyPattern.ReplaceAllString(rawQuery, `${1}[REDACTED]`) if redacted != rawQuery { c.Set("redactedQuery", redacted) } } // LogrusLogger Gin 请求日志中间件(使用 Logrus) func LogrusLogger(logger *logrus.Logger) gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() path := c.Request.URL.Path method := c.Request.Method // 处理请求 c.Next() // 计算延迟 latency := time.Since(start) statusCode := c.Writer.Status() clientIP := c.ClientIP() // 构建日志字段 fields := logrus.Fields{ "status": statusCode, "latency_ms": latency.Milliseconds(), "ip": clientIP, "method": method, "path": path, } // 添加请求 ID(如果存在) if requestID := getRequestID(c); requestID != "" { fields["request_id"] = requestID } // 添加脱敏后的数据 if redactedBody, exists := c.Get(RedactedBodyKey); exists { fields["body"] = redactedBody } if redactedAuth, exists := c.Get(RedactedAuthHeaderKey); exists { fields["authorization"] = redactedAuth } if redactedQuery, exists := c.Get("redactedQuery"); exists { fields["query"] = redactedQuery } // 添加用户信息(如果已认证) if user := getAuthenticatedUser(c); user != "" { fields["user"] = user } // 根据状态码选择日志级别 entry := logger.WithFields(fields) if len(c.Errors) > 0 { fields["errors"] = c.Errors.String() entry.Error("Request failed") } else { switch { case statusCode >= 500: entry.Error("Server error") case statusCode >= 400: entry.Warn("Client error") case statusCode >= 300: entry.Info("Redirect") default: // 只在 Debug 模式记录成功请求 if logger.Level >= logrus.DebugLevel { entry.Debug("Request completed") } } } } } // getRequestID 获取请求 ID func getRequestID(c *gin.Context) string { if id, exists := c.Get("request_id"); exists { if requestID, ok := id.(string); ok { return requestID } } return "" } // getAuthenticatedUser 获取已认证用户标识 func getAuthenticatedUser(c *gin.Context) string { // 尝试从不同来源获取用户信息 if user, exists := c.Get("adminUser"); exists { if authToken, ok := user.(interface{ GetID() string }); ok { return authToken.GetID() } } if user, exists := c.Get("authToken"); exists { if authToken, ok := user.(interface{ GetID() string }); ok { return authToken.GetID() } } return "" }