package middleware import ( "bytes" "io" "strings" "time" "carrot_bbs/internal/model" "carrot_bbs/internal/pkg/sanitizer" "github.com/gin-gonic/gin" ) // AccessLogConfig 访问日志配置 type AccessLogConfig struct { Enable bool SkipPaths []string ServerIP string ServerPort int } // AccessLogMiddleware 访问日志中间件 func AccessLogMiddleware(logService OperationLogRecorder, config *AccessLogConfig) gin.HandlerFunc { return func(c *gin.Context) { if !config.Enable { c.Next() return } // 跳过指定路径 for _, path := range config.SkipPaths { if strings.HasPrefix(c.Request.URL.Path, path) { c.Next() return } } start := time.Now() path := c.Request.URL.Path method := c.Request.Method // 读取请求体(用于记录) var requestBody string if c.Request.Body != nil { bodyBytes, _ := io.ReadAll(c.Request.Body) c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) requestBody = string(bodyBytes) } c.Next() // 记录日志 latency := time.Since(start) statusCode := c.Writer.Status() userID := "" if userIDVal, exists := c.Get("user_id"); exists { if uid, ok := userIDVal.(string); ok { userID = uid } } userName := "" if userNameVal, exists := c.Get("user_name"); exists { if name, ok := userNameVal.(string); ok { userName = name } } ip := c.ClientIP() userAgent := c.Request.UserAgent() referer := c.Request.Referer() // 确定操作类型 operation := determineOperation(method, path, c) // 脱敏处理 requestBody = sanitizeRequestBody(requestBody) // 构造日志对象 log := &model.OperationLog{ UserID: userID, UserName: sanitizer.MaskValue("username", userName), Operation: operation, Module: determineModule(path), Method: method, Path: path, QueryParams: c.Request.URL.RawQuery, RequestBody: requestBody, ResponseCode: statusCode, IP: ip, UserAgent: userAgent, Duration: latency.Milliseconds(), Referer: referer, Protocol: c.Request.Proto, ServerIP: config.ServerIP, ServerPort: config.ServerPort, Status: determineStatus(statusCode), OccurredAt: start, } // 异步记录日志 logService.RecordOperation(log) } } // OperationLogRecorder 操作日志记录器接口 type OperationLogRecorder interface { RecordOperation(log *model.OperationLog) } // determineOperation 确定操作类型 func determineOperation(method, path string, c *gin.Context) string { if method == "GET" { return string(model.OpUserLogin) } pathLower := strings.ToLower(path) switch { case strings.Contains(pathLower, "/posts"): if method == "POST" { return string(model.OpPostCreate) } else if method == "PUT" || method == "PATCH" { return string(model.OpPostUpdate) } else if method == "DELETE" { return string(model.OpPostDelete) } case strings.Contains(pathLower, "/comments"): if method == "POST" { return string(model.OpCommentCreate) } else if method == "DELETE" { return string(model.OpCommentDelete) } case strings.Contains(pathLower, "/users/me") || strings.Contains(pathLower, "/users/"): if strings.Contains(pathLower, "/avatar") { return string(model.OpAvatarUpload) } return string(model.OpProfileUpdate) case strings.Contains(pathLower, "/auth/register"): return string(model.OpUserRegister) case strings.Contains(pathLower, "/auth/login"): return string(model.OpUserLogin) case strings.Contains(pathLower, "/auth/logout"): return string(model.OpUserLogout) } return method + ":" + path } // determineModule 确定业务模块 func determineModule(path string) string { pathLower := strings.ToLower(path) switch { case strings.Contains(pathLower, "/posts"): return "post" case strings.Contains(pathLower, "/comments"): return "comment" case strings.Contains(pathLower, "/users"): return "user" case strings.Contains(pathLower, "/auth"): return "auth" case strings.Contains(pathLower, "/groups"): return "group" case strings.Contains(pathLower, "/admin"): return "admin" default: return "other" } } // determineStatus 确定状态 func determineStatus(statusCode int) string { if statusCode >= 400 { return "fail" } return "success" } // sanitizeRequestBody 脱敏请求体 func sanitizeRequestBody(body string) string { lines := strings.Split(body, "\n") sanitizedLines := make([]string, 0, len(lines)) for _, line := range lines { sanitizedLine := line parts := strings.Split(line, ":") if len(parts) >= 2 { field := strings.TrimSpace(strings.Trim(parts[0], `"'`)) if sanitizer.IsSensitiveField(field) { sanitizedLine = parts[0] + ": ***" } } sanitizedLines = append(sanitizedLines, sanitizedLine) } return strings.Join(sanitizedLines, "\n") }