package logger import ( "context" "net/url" "strings" "time" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/bytedance/sonic" "github.com/gofiber/fiber/v2" "go.uber.org/zap" ) const ( // MaxBodyLogSize 限制记录的请求/响应 body 大小为 50KB MaxBodyLogSize = 50 * 1024 ) // truncateBody 截断 body 到指定大小 func truncateBody(body []byte, maxSize int) string { if len(body) == 0 { return "" } if len(body) <= maxSize { return string(body) } // 超过限制,截断并添加提示 return string(body[:maxSize]) + "... (truncated)" } // maskSensitiveValue 按字段名判断并脱敏访问日志中的敏感值 func maskSensitiveValue(key, value string) string { if value == "" { return value } normalized := strings.ToLower(key) if strings.Contains(normalized, "password") || strings.Contains(normalized, "sign") || strings.Contains(normalized, "nonce") || strings.Contains(normalized, "token") || strings.Contains(normalized, "secret") { return "***" } return value } // sanitizeQuery 脱敏 query 中的密码、签名、nonce、token 等字段 func sanitizeQuery(rawQuery string) string { if rawQuery == "" { return "" } values, err := url.ParseQuery(rawQuery) if err != nil { return rawQuery } for key, items := range values { for index, item := range items { items[index] = maskSensitiveValue(key, item) } values[key] = items } return values.Encode() } // sanitizeBody 脱敏 JSON 请求体后再写入访问日志 func sanitizeBody(rawBody []byte) string { if len(rawBody) == 0 { return "" } var payload any if err := sonic.Unmarshal(rawBody, &payload); err != nil { return truncateBody(rawBody, MaxBodyLogSize) } sanitizeJSONValue(payload) data, err := sonic.Marshal(payload) if err != nil { return truncateBody(rawBody, MaxBodyLogSize) } return truncateBody(data, MaxBodyLogSize) } // sanitizeJSONValue 递归脱敏 JSON 对象中的敏感字段 func sanitizeJSONValue(value any) { switch typed := value.(type) { case map[string]any: for key, item := range typed { if masked, ok := item.(string); ok { typed[key] = maskSensitiveValue(key, masked) continue } if shouldMaskField(key) { typed[key] = "***" continue } sanitizeJSONValue(item) } case []any: for _, item := range typed { sanitizeJSONValue(item) } } } // shouldMaskField 判断字段名是否属于访问日志敏感字段 func shouldMaskField(key string) bool { normalized := strings.ToLower(key) return strings.Contains(normalized, "password") || strings.Contains(normalized, "sign") || strings.Contains(normalized, "nonce") || strings.Contains(normalized, "token") || strings.Contains(normalized, "secret") } // Middleware 创建 Fiber 日志中间件 // 记录所有 HTTP 请求到访问日志(包括请求和响应 body) func Middleware() fiber.Handler { return func(c *fiber.Ctx) error { // 记录请求开始时间 startTime := time.Now() c.Locals(constants.ContextKeyStartTime, startTime) // 注入请求上下文,供 Service 层审计日志复用 ctx := c.UserContext() if rid := c.Locals(constants.ContextKeyRequestID); rid != nil { if requestID, ok := rid.(string); ok && requestID != "" { ctx = context.WithValue(ctx, constants.ContextKeyRequestID, requestID) } } ctx = context.WithValue(ctx, constants.ContextKeyIP, c.IP()) ctx = context.WithValue(ctx, constants.ContextKeyUserAgent, c.Get("User-Agent")) ctx = context.WithValue(ctx, constants.ContextKeyRequestPath, c.Path()) ctx = context.WithValue(ctx, constants.ContextKeyRequestMethod, c.Method()) c.SetUserContext(ctx) // 获取请求 body(在 c.Next() 之前读取) requestBody := sanitizeBody(c.Body()) // 获取 query 参数 queryParams := sanitizeQuery(string(c.Request().URI().QueryString())) // 处理请求 err := c.Next() // 计算请求持续时间 duration := time.Since(startTime) // 获取请求 ID(由 requestid 中间件设置) requestID := "" if rid := c.Locals(constants.ContextKeyRequestID); rid != nil { requestID = rid.(string) } // 获取用户 ID(由 auth 中间件设置) var userID uint if uid := c.Locals(constants.ContextKeyUserID); uid != nil { if id, ok := uid.(uint); ok { userID = id } } // 获取响应 body responseBody := truncateBody(c.Response().Body(), MaxBodyLogSize) // 记录访问日志 accessLogger := GetAccessLogger() accessLogger.Info("", zap.String("method", c.Method()), zap.String("path", c.Path()), zap.String("query", queryParams), zap.Int("status", c.Response().StatusCode()), zap.Float64("duration_ms", float64(duration.Microseconds())/1000.0), zap.String("request_id", requestID), zap.String("ip", c.IP()), zap.String("user_agent", c.Get("User-Agent")), zap.Uint("user_id", userID), zap.String("request_body", requestBody), zap.String("response_body", responseBody), ) return err } }