feat: 添加日志管理和敏感词过滤功能
- 新增日志管理模块:操作日志、登录日志、数据变更日志 - 新增敏感词过滤器 (sanitizer) - 新增日志异步写入管理器 - 新增日志定时清理服务 - 优化帖子相关服务和上传服务
This commit is contained in:
200
internal/middleware/access_log.go
Normal file
200
internal/middleware/access_log.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user