feat: 添加日志管理和敏感词过滤功能

- 新增日志管理模块:操作日志、登录日志、数据变更日志
- 新增敏感词过滤器 (sanitizer)
- 新增日志异步写入管理器
- 新增日志定时清理服务
- 优化帖子相关服务和上传服务
This commit is contained in:
2026-03-15 02:25:10 +08:00
parent d3065b30cc
commit ebd9cb8b04
31 changed files with 2753 additions and 60 deletions

View File

@@ -0,0 +1,200 @@
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")
}