/** * package middleware * 作用:接口请求日志中间件 * 说明:记录所有API请求信息,异步写入数据库 */ package middleware import ( "bytes" "encoding/json" "fmt" "io" "strings" "time" "xk-websocket-v2/internal/model" "xk-websocket-v2/internal/utils" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // 二进制 Content-Type 前缀列表 var binaryContentTypes = []string{ "image/", "audio/", "video/", "application/octet-stream", "application/pdf", "application/zip", "application/x-rar", "application/x-7z", "application/gzip", "application/x-tar", "font/", } // 不需要记录响应体的路由前缀 var skipResponseBodyRoutes = []string{ "/uploads/", "/static/", } // 需要完全跳过中间件的路由(如 WebSocket) var skipMiddlewareRoutes = []string{ "/ws", "/api/call/ws-push", } // isBinaryContentType 检测是否为二进制 Content-Type func isBinaryContentType(contentType string) bool { contentType = strings.ToLower(contentType) for _, prefix := range binaryContentTypes { if strings.HasPrefix(contentType, prefix) { return true } } return false } // isBinaryData 检测数据是否为二进制(通过检查是否包含非 UTF-8 字符) func isBinaryData(data []byte) bool { if len(data) == 0 { return false } // 检查前 512 字节是否包含二进制特征 checkLen := len(data) if checkLen > 512 { checkLen = 512 } for i := 0; i < checkLen; i++ { // 检测常见的二进制文件头 if data[i] == 0 { return true } } // 检查是否以常见的二进制文件头开始 if len(data) >= 2 { // JPEG: FF D8 if data[0] == 0xFF && data[1] == 0xD8 { return true } // PNG: 89 50 if data[0] == 0x89 && data[1] == 0x50 { return true } // GIF: 47 49 if data[0] == 0x47 && data[1] == 0x49 { return true } // PDF: 25 50 if data[0] == 0x25 && data[1] == 0x50 { return true } // ZIP/DOCX/XLSX: 50 4B if data[0] == 0x50 && data[1] == 0x4B { return true } } return false } // shouldSkipResponseBody 检测是否应该跳过响应体记录 func shouldSkipResponseBody(path string) bool { for _, prefix := range skipResponseBodyRoutes { if strings.HasPrefix(path, prefix) { return true } } return false } // RequestLogMiddleware 请求日志中间件 type RequestLogMiddleware struct { DB *gorm.DB } // NewRequestLogMiddleware 创建请求日志中间件 func NewRequestLogMiddleware(db *gorm.DB) *RequestLogMiddleware { return &RequestLogMiddleware{DB: db} } // shouldSkipMiddleware 检测是否应该完全跳过中间件 func shouldSkipMiddleware(path string) bool { for _, route := range skipMiddlewareRoutes { if strings.HasPrefix(path, route) { return true } } return false } // Handler 中间件处理函数 func (m *RequestLogMiddleware) Handler() gin.HandlerFunc { return func(c *gin.Context) { // 跳过OPTIONS请求 if c.Request.Method == "OPTIONS" { c.Next() return } // 跳过 WebSocket 路由(包装 Writer 会干扰 WebSocket 升级) if shouldSkipMiddleware(c.Request.URL.Path) { c.Next() return } // 获取请求IP ip := getClientIP(c) // 本地IP不记录 if utils.GetIPLocation(ip) == "本地" { c.Next() return } // 获取请求参数 var requestBody []byte if c.Request.Body != nil { requestBody, _ = io.ReadAll(c.Request.Body) c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) } // 获取用户ID(从Context中获取,未登录为"0") userID := "0" if uid, exists := c.Get("user_id"); exists { if uidStr, ok := uid.(string); ok { userID = uidStr } } // 记录开始时间 startTime := time.Now() // 创建响应写入器 writer := &responseWriter{ ResponseWriter: c.Writer, body: &bytes.Buffer{}, } c.Writer = writer // 处理请求 c.Next() // 计算请求时间 duration := time.Since(startTime) // 异步记录日志(避免影响性能) go m.logRequest(c, ip, userID, requestBody, writer.body.Bytes(), writer.status, duration) } } // logRequest 记录请求日志 func (m *RequestLogMiddleware) logRequest(c *gin.Context, ip, userID string, requestBody, responseBody []byte, httpStatus int, duration time.Duration) { // 获取IP归属地 location := utils.GetIPLocation(ip) // 获取响应code(从响应体中解析) responseCode := 0 if len(responseBody) > 0 && !isBinaryData(responseBody) { // 尝试解析响应体获取code(仅对非二进制数据) var resp model.ApiResponse if err := json.Unmarshal(responseBody, &resp); err == nil { responseCode = resp.Code } } // 处理请求参数 var requestParams string requestContentType := c.GetHeader("Content-Type") if isBinaryContentType(requestContentType) || isBinaryData(requestBody) { // 二进制请求体,只记录大小 requestParams = fmt.Sprintf("[binary data: %d bytes]", len(requestBody)) } else { requestParams = string(requestBody) if len(requestParams) > 5000 { requestParams = requestParams[:5000] + "...(truncated)" } } // 处理响应参数 var responseParams string responseContentType := c.Writer.Header().Get("Content-Type") requestPath := c.Request.URL.Path if shouldSkipResponseBody(requestPath) { // 静态文件路由,跳过响应体记录 responseParams = fmt.Sprintf("[static file: %d bytes]", len(responseBody)) } else if isBinaryContentType(responseContentType) || isBinaryData(responseBody) { // 二进制响应体,只记录大小 responseParams = fmt.Sprintf("[binary data: %d bytes]", len(responseBody)) } else { responseParams = string(responseBody) if len(responseParams) > 5000 { responseParams = responseParams[:5000] + "...(truncated)" } } // 创建日志记录 log := model.ApiRequestLog{ Route: c.FullPath(), IP: ip, IPLocation: location, UserID: userID, Method: c.Request.Method, RequestParams: requestParams, ResponseParams: responseParams, ResponseCode: responseCode, HTTPStatus: httpStatus, RequestTime: time.Now(), } // 异步写入数据库 m.DB.Create(&log) } // getClientIP 获取客户端IP func getClientIP(c *gin.Context) string { // 优先从X-Forwarded-For获取 ip := c.GetHeader("X-Forwarded-For") if ip != "" { // X-Forwarded-For可能包含多个IP,取第一个 ips := strings.Split(ip, ",") if len(ips) > 0 { return strings.TrimSpace(ips[0]) } } // 从X-Real-IP获取 ip = c.GetHeader("X-Real-IP") if ip != "" { return ip } // 从RemoteAddr获取 return c.ClientIP() } // responseWriter 响应写入器(用于捕获响应内容) type responseWriter struct { gin.ResponseWriter body *bytes.Buffer status int } func (w *responseWriter) Write(b []byte) (int, error) { w.body.Write(b) return w.ResponseWriter.Write(b) } func (w *responseWriter) WriteString(s string) (int, error) { w.body.WriteString(s) return w.ResponseWriter.WriteString(s) } func (w *responseWriter) WriteHeader(statusCode int) { w.status = statusCode w.ResponseWriter.WriteHeader(statusCode) }