277 lines
6.5 KiB
Go
277 lines
6.5 KiB
Go
/**
|
||
* 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 := utils.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)
|
||
}
|
||
|
||
// 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)
|
||
}
|