package middleware import ( "bytes" "encoding/json" "io" "net/http" "net/url" "wails-app/internal/dao" "strconv" "strings" "time" "wails-app/internal/pkg" "github.com/gin-gonic/gin" "go.uber.org/zap" "wails-app/internal/global" "wails-app/internal/service" ) var operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService var bufferSize = 1024 // sensitiveFields 操作日志脱敏字段(不区分大小写):密码、令牌、密钥、支付敏感字段。 var sensitiveFields = map[string]bool{ "password": true, "old_password": true, "new_password": true, "confirm_password": true, "pwd": true, "pass": true, "token": true, "access_token": true, "refresh_token": true, "jwt": true, "secret": true, "private_key": true, } // maskSensitiveJSON 对 JSON 文本中的敏感字段值脱敏(非 JSON 原样返回)。 func maskSensitiveJSON(data []byte) []byte { var obj interface{} if err := json.Unmarshal(data, &obj); err != nil { return data } out, err := json.Marshal(maskValue(obj)) if err != nil { return data } return out } // maskValue 递归脱敏:命中敏感字段名时值替换为 "******"(空值保留)。 func maskValue(v interface{}) interface{} { switch value := v.(type) { case map[string]interface{}: for key, child := range value { if sensitiveFields[strings.ToLower(key)] { if child != nil { value[key] = "******" } continue } value[key] = maskValue(child) } return value case []interface{}: for i := range value { value[i] = maskValue(value[i]) } return value default: return v } } // maskResponse 响应体脱敏并截断(修复旧代码只对下载类响应截断、且误写 record.Body 字段的问题)。 func maskResponse(resp string) string { masked := maskSensitiveJSON([]byte(resp)) if len(masked) > bufferSize { return "[超出记录长度]" } return string(masked) } func OperationRecord() gin.HandlerFunc { return func(c *gin.Context) { var body []byte var userId int if c.Request.Method != http.MethodGet { var err error body, err = io.ReadAll(c.Request.Body) if err != nil { global.GVA_LOG.Error("read body from request error:", zap.Error(err)) } else { c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) } } else { query := c.Request.URL.RawQuery query, _ = url.QueryUnescape(query) split := strings.Split(query, "&") m := make(map[string]string) for _, v := range split { kv := strings.Split(v, "=") if len(kv) == 2 { m[kv[0]] = kv[1] } } body, _ = json.Marshal(&m) } claims, _ := utils.GetClaims(c) if claims != nil && claims.BaseClaims.ID != 0 { userId = int(claims.BaseClaims.ID) } else { id, err := strconv.Atoi(c.Request.Header.Get("x-user-id")) if err != nil { userId = 0 } userId = id } record := dao.SysOperationRecord{ Ip: c.ClientIP(), Method: c.Request.Method, Path: c.Request.URL.Path, Agent: c.Request.UserAgent(), Body: "", UserID: userId, } // 上传文件时候 中间件日志进行裁断操作 if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") { record.Body = "[文件]" } else { masked := maskSensitiveJSON(body) if len(masked) > bufferSize { record.Body = "[超出记录长度]" } else { record.Body = string(masked) } } writer := responseBodyWriter{ ResponseWriter: c.Writer, body: &bytes.Buffer{}, } c.Writer = writer now := time.Now() c.Next() latency := time.Since(now) record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String() record.Status = c.Writer.Status() record.Latency = latency record.Resp = maskResponse(writer.body.String()) if err := operationRecordService.CreateSysOperationRecord(record); err != nil { global.GVA_LOG.Error("create operation record error:", zap.Error(err)) } } } type responseBodyWriter struct { gin.ResponseWriter body *bytes.Buffer } func (r responseBodyWriter) Write(b []byte) (int, error) { r.body.Write(b) return r.ResponseWriter.Write(b) }