operation.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. package middleware
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "wails-app/internal/dao"
  12. "wails-app/internal/pkg"
  13. "github.com/gin-gonic/gin"
  14. "go.uber.org/zap"
  15. "wails-app/internal/global"
  16. "wails-app/internal/service"
  17. )
  18. var operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService
  19. var bufferSize = 1024
  20. // sensitiveFields 操作日志脱敏字段(不区分大小写):密码、令牌、密钥、支付敏感字段。
  21. var sensitiveFields = map[string]bool{
  22. "password": true, "old_password": true, "new_password": true,
  23. "confirm_password": true, "pwd": true, "pass": true,
  24. "pairing_code": true,
  25. "token": true, "access_token": true, "refresh_token": true, "jwt": true,
  26. "secret": true, "private_key": true,
  27. }
  28. // maskSensitiveJSON 对 JSON 文本中的敏感字段值脱敏(非 JSON 原样返回)。
  29. func maskSensitiveJSON(data []byte) []byte {
  30. var obj interface{}
  31. if err := json.Unmarshal(data, &obj); err != nil {
  32. return data
  33. }
  34. out, err := json.Marshal(maskValue(obj))
  35. if err != nil {
  36. return data
  37. }
  38. return out
  39. }
  40. // maskValue 递归脱敏:命中敏感字段名时值替换为 "******"(空值保留)。
  41. func maskValue(v interface{}) interface{} {
  42. switch value := v.(type) {
  43. case map[string]interface{}:
  44. for key, child := range value {
  45. if sensitiveFields[strings.ToLower(key)] {
  46. if child != nil {
  47. value[key] = "******"
  48. }
  49. continue
  50. }
  51. value[key] = maskValue(child)
  52. }
  53. return value
  54. case []interface{}:
  55. for i := range value {
  56. value[i] = maskValue(value[i])
  57. }
  58. return value
  59. default:
  60. return v
  61. }
  62. }
  63. // maskResponse 响应体脱敏并截断(修复旧代码只对下载类响应截断、且误写 record.Body 字段的问题)。
  64. func maskResponse(resp string) string {
  65. masked := maskSensitiveJSON([]byte(resp))
  66. if len(masked) > bufferSize {
  67. return "[超出记录长度]"
  68. }
  69. return string(masked)
  70. }
  71. func OperationRecord() gin.HandlerFunc {
  72. return func(c *gin.Context) {
  73. // GET 请求是只读查询,很多页面会周期性轮询。避免为每次查询
  74. // 同步写入操作日志,减少 SQLite 写锁竞争;写操作仍完整审计。
  75. if c.Request.Method == http.MethodGet {
  76. c.Next()
  77. return
  78. }
  79. var body []byte
  80. var userId int
  81. if c.Request.Method != http.MethodGet {
  82. var err error
  83. body, err = io.ReadAll(c.Request.Body)
  84. if err != nil {
  85. global.GVA_LOG.Error("read body from request error:", zap.Error(err))
  86. } else {
  87. c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
  88. }
  89. } else {
  90. query := c.Request.URL.RawQuery
  91. query, _ = url.QueryUnescape(query)
  92. split := strings.Split(query, "&")
  93. m := make(map[string]string)
  94. for _, v := range split {
  95. kv := strings.Split(v, "=")
  96. if len(kv) == 2 {
  97. m[kv[0]] = kv[1]
  98. }
  99. }
  100. body, _ = json.Marshal(&m)
  101. }
  102. claims, _ := utils.GetClaims(c)
  103. if claims != nil && claims.BaseClaims.ID != 0 {
  104. userId = int(claims.BaseClaims.ID)
  105. } else {
  106. id, err := strconv.Atoi(c.Request.Header.Get("x-user-id"))
  107. if err != nil {
  108. userId = 0
  109. }
  110. userId = id
  111. }
  112. record := dao.SysOperationRecord{
  113. Ip: c.ClientIP(),
  114. Method: c.Request.Method,
  115. Path: c.Request.URL.Path,
  116. Agent: c.Request.UserAgent(),
  117. Body: "",
  118. UserID: userId,
  119. }
  120. // 上传文件时候 中间件日志进行裁断操作
  121. if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
  122. record.Body = "[文件]"
  123. } else {
  124. masked := maskSensitiveJSON(body)
  125. if len(masked) > bufferSize {
  126. record.Body = "[超出记录长度]"
  127. } else {
  128. record.Body = string(masked)
  129. }
  130. }
  131. writer := responseBodyWriter{
  132. ResponseWriter: c.Writer,
  133. body: &bytes.Buffer{},
  134. }
  135. c.Writer = writer
  136. now := time.Now()
  137. c.Next()
  138. latency := time.Since(now)
  139. record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String()
  140. record.Status = c.Writer.Status()
  141. record.Latency = latency
  142. record.Resp = maskResponse(writer.body.String())
  143. if err := operationRecordService.CreateSysOperationRecord(record); err != nil {
  144. global.GVA_LOG.Error("create operation record error:", zap.Error(err))
  145. }
  146. }
  147. }
  148. type responseBodyWriter struct {
  149. gin.ResponseWriter
  150. body *bytes.Buffer
  151. }
  152. func (r responseBodyWriter) Write(b []byte) (int, error) {
  153. r.body.Write(b)
  154. return r.ResponseWriter.Write(b)
  155. }