package logger import ( "context" "crypto/sha256" "encoding/hex" "net/url" "time" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/sanitizer" "github.com/bytedance/sonic" "github.com/gofiber/fiber/v2" "go.uber.org/zap" ) const ( // MaxBodyLogSize 限制记录的请求/响应 body 大小为 50KB MaxBodyLogSize = 50 * 1024 redactedValue = "[已脱敏]" ) // BodyPolicy 表示访问日志正文记录策略。 type BodyPolicy int const ( // BodyPolicyJSON 表示只允许记录脱敏后的 JSON 正文。 BodyPolicyJSON BodyPolicy = iota + 1 // BodyPolicySummary 表示只记录不可逆安全摘要。 BodyPolicySummary ) // SanitizedContent 表示访问日志可安全记录的正文或查询摘要。 type SanitizedContent struct { Content string Size int SHA256 string Truncated bool } // truncateBody 截断 body 到指定大小 func truncateBody(body []byte, maxSize int) (string, bool) { if len(body) == 0 { return "", false } if len(body) <= maxSize { return string(body), false } return string(body[:maxSize]), true } // maskSensitiveValue 按字段名判断并脱敏访问日志中的敏感值 func maskSensitiveValue(key, value string) string { if value == "" { return value } if shouldMaskField(key) { return redactedValue } return value } // sanitizeQuery 脱敏 query 中的密码、签名、nonce、token 等字段 func sanitizeQuery(rawQuery string) SanitizedContent { if rawQuery == "" { return SanitizedContent{} } values, err := url.ParseQuery(rawQuery) if err != nil { return summarize([]byte(rawQuery)) } for key, items := range values { for index, item := range items { items[index] = maskSensitiveValue(key, item) } values[key] = items } content, truncated := truncateBody([]byte(values.Encode()), MaxBodyLogSize) return SanitizedContent{Content: content, Size: len(rawQuery), SHA256: digest([]byte(rawQuery)), Truncated: truncated} } // sanitizeBody 按策略脱敏正文后再写入访问日志。 func sanitizeBody(rawBody []byte, policy BodyPolicy) SanitizedContent { if len(rawBody) == 0 { return SanitizedContent{} } if policy == BodyPolicySummary { return summarize(rawBody) } var payload any if err := sonic.Unmarshal(rawBody, &payload); err != nil { return summarize(rawBody) } sanitizeJSONValue(payload) data, err := sonic.Marshal(payload) if err != nil { return summarize(rawBody) } content, truncated := truncateBody(data, MaxBodyLogSize) return SanitizedContent{Content: content, Size: len(rawBody), SHA256: digest(rawBody), Truncated: truncated} } func summarize(raw []byte) SanitizedContent { return SanitizedContent{Content: "[仅记录安全摘要]", Size: len(raw), SHA256: digest(raw), Truncated: len(raw) > MaxBodyLogSize} } func digest(raw []byte) string { sum := sha256.Sum256(raw) return hex.EncodeToString(sum[:]) } // 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] = redactedValue continue } sanitizeJSONValue(item) } case []any: for _, item := range typed { sanitizeJSONValue(item) } } } // shouldMaskField 判断字段名是否属于访问日志敏感字段 func shouldMaskField(key string) bool { return sanitizer.IsForbiddenField(key) } // Middleware 创建 Fiber 日志中间件 // 记录所有 HTTP 请求到访问日志(包括请求和响应 body) func Middleware() fiber.Handler { return MiddlewareWithLogger(GetAccessLogger()) } // MiddlewareWithLogger 创建可注入访问日志器的 Fiber 中间件。 // 生产环境使用 Middleware;该入口让集成测试捕获最终 JSON 日志而无需修改全局状态。 func MiddlewareWithLogger(accessLogger *zap.Logger) 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() 之前读取) policy := policyForPath(c.Path()) requestBody := sanitizeWithPolicy(c.Body(), c.Get("Content-Type"), policy) // 获取 query 参数 queryParams := sanitizeQuery(string(c.Request().URI().QueryString())) if policy.Sensitive && queryParams.Size > 0 { queryParams = summarize([]byte(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 := sanitizeWithPolicy(c.Response().Body(), string(c.Response().Header.ContentType()), policy) // 记录访问日志 accessLogger.Info("", zap.String("method", c.Method()), zap.String("path", c.Path()), zap.String("body_policy", policy.Name), zap.String("query", queryParams.Content), zap.Bool("query_truncated", queryParams.Truncated), 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.Content), zap.Int("request_body_size", requestBody.Size), zap.String("request_body_sha256", requestBody.SHA256), zap.Bool("request_body_truncated", requestBody.Truncated), zap.String("response_body", responseBody.Content), zap.Int("response_body_size", responseBody.Size), zap.String("response_body_sha256", responseBody.SHA256), zap.Bool("response_body_truncated", responseBody.Truncated), ) return err } }