- 新增日志管理模块:操作日志、登录日志、数据变更日志 - 新增敏感词过滤器 (sanitizer) - 新增日志异步写入管理器 - 新增日志定时清理服务 - 优化帖子相关服务和上传服务
201 lines
4.8 KiB
Go
201 lines
4.8 KiB
Go
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")
|
|
}
|