|
@@ -2,8 +2,10 @@ package middleware
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
+ "encoding/json"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
+ "mime/multipart"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
@@ -36,14 +38,6 @@ var (
|
|
|
"user-agent",
|
|
|
"content-type",
|
|
|
}
|
|
|
-
|
|
|
- // 敏感字段
|
|
|
- sensitiveFields = []string{
|
|
|
- "password", "passwd", "pwd",
|
|
|
- "token", "access_token", "refresh_token",
|
|
|
- "secret", "api_key", "apikey",
|
|
|
- "authorization",
|
|
|
- }
|
|
|
)
|
|
|
|
|
|
func RequestLogMiddleware(logger *log.Logger) gin.HandlerFunc {
|
|
@@ -78,8 +72,23 @@ func RequestLogMiddleware(logger *log.Logger) gin.HandlerFunc {
|
|
|
|
|
|
// 记录请求体(仅限特定方法)
|
|
|
if shouldLogRequestBody(ctx) {
|
|
|
- if bodyLog := getRequestBody(ctx); bodyLog != "" {
|
|
|
- fields = append(fields, zap.String("body", bodyLog))
|
|
|
+ contentType := ctx.GetHeader("Content-Type")
|
|
|
+
|
|
|
+ // 特殊处理 multipart/form-data
|
|
|
+ if strings.Contains(contentType, "multipart/form-data") {
|
|
|
+ if formData := parseMultipartData(ctx); formData != nil {
|
|
|
+ fields = append(fields, zap.Any("form_data", formData))
|
|
|
+ }
|
|
|
+ } else if strings.Contains(contentType, "application/json") {
|
|
|
+ // 处理 JSON 请求体
|
|
|
+ if bodyData := getJSONBody(ctx); bodyData != nil {
|
|
|
+ fields = append(fields, zap.Any("body", bodyData))
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 处理其他类型的请求体
|
|
|
+ if bodyLog := getRequestBody(ctx); bodyLog != "" {
|
|
|
+ fields = append(fields, zap.String("body", bodyLog))
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -133,7 +142,17 @@ func ResponseLogMiddleware(logger *log.Logger) gin.HandlerFunc {
|
|
|
if len(bodyStr) > MaxBodySize {
|
|
|
fields = append(fields, zap.String("body", fmt.Sprintf("[TRUNCATED: %d bytes]", len(bodyStr))))
|
|
|
} else if len(bodyStr) > 0 {
|
|
|
- fields = append(fields, zap.String("body", maskSensitiveData(bodyStr)))
|
|
|
+ // 尝试解析 JSON 响应
|
|
|
+ if json.Valid([]byte(bodyStr)) {
|
|
|
+ var jsonData interface{}
|
|
|
+ if err := json.Unmarshal([]byte(bodyStr), &jsonData); err == nil {
|
|
|
+ fields = append(fields, zap.Any("body", jsonData))
|
|
|
+ } else {
|
|
|
+ fields = append(fields, zap.String("body", bodyStr))
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ fields = append(fields, zap.String("body", bodyStr))
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -157,6 +176,115 @@ func ResponseLogMiddleware(logger *log.Logger) gin.HandlerFunc {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 获取 JSON 请求体并解析为 map
|
|
|
+func getJSONBody(ctx *gin.Context) interface{} {
|
|
|
+ if ctx.Request.Body == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ bodyBytes, err := ctx.GetRawData()
|
|
|
+ if err != nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置请求体
|
|
|
+ ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
|
+
|
|
|
+ // 检查大小
|
|
|
+ if len(bodyBytes) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ if len(bodyBytes) > MaxBodySize {
|
|
|
+ return fmt.Sprintf("[TRUNCATED: %d bytes]", len(bodyBytes))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析 JSON
|
|
|
+ var data interface{}
|
|
|
+ if err := json.Unmarshal(bodyBytes, &data); err != nil {
|
|
|
+ // 如果解析失败,返回原始字符串
|
|
|
+ return string(bodyBytes)
|
|
|
+ }
|
|
|
+
|
|
|
+ return data
|
|
|
+}
|
|
|
+
|
|
|
+// 解析 multipart/form-data
|
|
|
+func parseMultipartData(ctx *gin.Context) map[string]interface{} {
|
|
|
+ // 保存原始请求体
|
|
|
+ bodyBytes, err := ctx.GetRawData()
|
|
|
+ if err != nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置请求体
|
|
|
+ ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
|
+
|
|
|
+ // 创建新的 reader
|
|
|
+ reader := multipart.NewReader(bytes.NewReader(bodyBytes), extractBoundary(ctx.GetHeader("Content-Type")))
|
|
|
+ if reader == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ formData := make(map[string]interface{})
|
|
|
+
|
|
|
+ for {
|
|
|
+ part, err := reader.NextPart()
|
|
|
+ if err == io.EOF {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ name := part.FormName()
|
|
|
+ if name == "" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取内容
|
|
|
+ value, err := io.ReadAll(part)
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ valueStr := string(value)
|
|
|
+
|
|
|
+ // 尝试解析为 JSON
|
|
|
+ if json.Valid(value) {
|
|
|
+ var jsonData interface{}
|
|
|
+ if err := json.Unmarshal(value, &jsonData); err == nil {
|
|
|
+ formData[name] = jsonData
|
|
|
+ } else {
|
|
|
+ formData[name] = valueStr
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ formData[name] = valueStr
|
|
|
+ }
|
|
|
+
|
|
|
+ part.Close()
|
|
|
+ }
|
|
|
+
|
|
|
+ return formData
|
|
|
+}
|
|
|
+
|
|
|
+// 提取 boundary
|
|
|
+func extractBoundary(contentType string) string {
|
|
|
+ if !strings.Contains(contentType, "boundary=") {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+ parts := strings.Split(contentType, "boundary=")
|
|
|
+ if len(parts) < 2 {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+ boundary := parts[1]
|
|
|
+ // 移除可能的引号
|
|
|
+ boundary = strings.Trim(boundary, `"`)
|
|
|
+
|
|
|
+ return boundary
|
|
|
+}
|
|
|
+
|
|
|
// bodyLogWriter 包装响应写入器以捕获响应体
|
|
|
type bodyLogWriter struct {
|
|
|
gin.ResponseWriter
|
|
@@ -219,100 +347,5 @@ func getRequestBody(ctx *gin.Context) string {
|
|
|
return fmt.Sprintf("[TRUNCATED: %d bytes]", len(bodyBytes))
|
|
|
}
|
|
|
|
|
|
- // 脱敏处理
|
|
|
- return maskSensitiveData(string(bodyBytes))
|
|
|
-}
|
|
|
-
|
|
|
-func maskSensitiveData(data string) string {
|
|
|
- result := data
|
|
|
- for _, field := range sensitiveFields {
|
|
|
- // 简单的JSON字段脱敏
|
|
|
- result = maskJSONField(result, field)
|
|
|
- // URL参数脱敏
|
|
|
- result = maskURLParam(result, field)
|
|
|
- }
|
|
|
- return result
|
|
|
-}
|
|
|
-
|
|
|
-func maskJSONField(data, field string) string {
|
|
|
- lowerData := strings.ToLower(data)
|
|
|
- lowerField := strings.ToLower(field)
|
|
|
-
|
|
|
- // 查找字段位置(不区分大小写)
|
|
|
- idx := strings.Index(lowerData, `"`+lowerField+`"`)
|
|
|
- if idx == -1 {
|
|
|
- idx = strings.Index(lowerData, `'`+lowerField+`'`)
|
|
|
- if idx == -1 {
|
|
|
- return data
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 找到冒号位置
|
|
|
- colonIdx := strings.Index(data[idx:], ":")
|
|
|
- if colonIdx == -1 {
|
|
|
- return data
|
|
|
- }
|
|
|
- colonIdx += idx
|
|
|
-
|
|
|
- // 找到值的开始和结束位置
|
|
|
- valueStart := colonIdx + 1
|
|
|
- for valueStart < len(data) && (data[valueStart] == ' ' || data[valueStart] == '\t') {
|
|
|
- valueStart++
|
|
|
- }
|
|
|
-
|
|
|
- if valueStart >= len(data) {
|
|
|
- return data
|
|
|
- }
|
|
|
-
|
|
|
- // 判断值的类型
|
|
|
- var valueEnd int
|
|
|
- if data[valueStart] == '"' || data[valueStart] == '\'' {
|
|
|
- // 字符串值
|
|
|
- quote := data[valueStart]
|
|
|
- valueEnd = valueStart + 1
|
|
|
- for valueEnd < len(data) && data[valueEnd] != quote {
|
|
|
- if data[valueEnd] == '\\' {
|
|
|
- valueEnd++ // 跳过转义字符
|
|
|
- }
|
|
|
- valueEnd++
|
|
|
- }
|
|
|
- if valueEnd < len(data) {
|
|
|
- valueEnd++ // 包含结束引号
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 非字符串值(数字、布尔值等)
|
|
|
- valueEnd = valueStart
|
|
|
- for valueEnd < len(data) && data[valueEnd] != ',' && data[valueEnd] != '}' && data[valueEnd] != ']' && data[valueEnd] != '\n' && data[valueEnd] != '\r' {
|
|
|
- valueEnd++
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 替换为脱敏值
|
|
|
- return data[:valueStart] + `"***"` + data[valueEnd:]
|
|
|
-}
|
|
|
-
|
|
|
-func maskURLParam(data, param string) string {
|
|
|
- lowerData := strings.ToLower(data)
|
|
|
- lowerParam := strings.ToLower(param)
|
|
|
-
|
|
|
- // 查找参数位置
|
|
|
- idx := strings.Index(lowerData, lowerParam+"=")
|
|
|
- if idx == -1 {
|
|
|
- return data
|
|
|
- }
|
|
|
-
|
|
|
- // 确保是参数开始位置(前面是?或&)
|
|
|
- if idx > 0 && data[idx-1] != '?' && data[idx-1] != '&' && data[idx-1] != ' ' && data[idx-1] != '\n' {
|
|
|
- return data
|
|
|
- }
|
|
|
-
|
|
|
- // 找到参数值结束位置
|
|
|
- valueStart := idx + len(param) + 1
|
|
|
- valueEnd := valueStart
|
|
|
- for valueEnd < len(data) && data[valueEnd] != '&' && data[valueEnd] != ' ' && data[valueEnd] != '\n' && data[valueEnd] != '\r' {
|
|
|
- valueEnd++
|
|
|
- }
|
|
|
-
|
|
|
- // 替换为脱敏值
|
|
|
- return data[:valueStart] + "***" + data[valueEnd:]
|
|
|
+ return string(bodyBytes)
|
|
|
}
|