214 lines
5.1 KiB
Go
214 lines
5.1 KiB
Go
// Filename: internal/middleware/logging.go
|
||
|
||
package middleware
|
||
|
||
import (
|
||
"bytes"
|
||
"io"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/sirupsen/logrus"
|
||
)
|
||
|
||
const (
|
||
RedactedBodyKey = "redactedBody"
|
||
RedactedAuthHeaderKey = "redactedAuthHeader"
|
||
RedactedValue = `"[REDACTED]"`
|
||
)
|
||
|
||
// 预编译正则表达式(全局变量,提升性能)
|
||
var (
|
||
// JSON 敏感字段脱敏
|
||
jsonSensitiveKeys = regexp.MustCompile(`("(?i:api_key|apikey|token|password|secret|authorization|key|keys|auth)"\s*:\s*)"[^"]*"`)
|
||
|
||
// Bearer Token 脱敏
|
||
bearerTokenPattern = regexp.MustCompile(`^(Bearer\s+)\S+$`)
|
||
|
||
// URL 中的 key 参数脱敏
|
||
queryKeyPattern = regexp.MustCompile(`([?&](?i:key|token|apikey)=)[^&\s]+`)
|
||
)
|
||
|
||
// RedactionMiddleware 请求数据脱敏中间件
|
||
func RedactionMiddleware() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// 1. 脱敏请求体
|
||
if shouldRedactBody(c) {
|
||
redactRequestBody(c)
|
||
}
|
||
|
||
// 2. 脱敏认证头
|
||
redactAuthHeader(c)
|
||
|
||
// 3. 脱敏 URL 查询参数
|
||
redactQueryParams(c)
|
||
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// shouldRedactBody 判断是否需要脱敏请求体
|
||
func shouldRedactBody(c *gin.Context) bool {
|
||
method := c.Request.Method
|
||
contentType := c.GetHeader("Content-Type")
|
||
|
||
// 只处理包含 JSON 的 POST/PUT/PATCH/DELETE 请求
|
||
return (method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE") &&
|
||
strings.Contains(contentType, "application/json")
|
||
}
|
||
|
||
// redactRequestBody 脱敏请求体
|
||
func redactRequestBody(c *gin.Context) {
|
||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 恢复请求体供后续使用
|
||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||
|
||
// 脱敏敏感字段
|
||
bodyString := string(bodyBytes)
|
||
redactedBody := jsonSensitiveKeys.ReplaceAllString(bodyString, `$1`+RedactedValue)
|
||
|
||
c.Set(RedactedBodyKey, redactedBody)
|
||
}
|
||
|
||
// redactAuthHeader 脱敏认证头
|
||
func redactAuthHeader(c *gin.Context) {
|
||
authHeader := c.GetHeader("Authorization")
|
||
if authHeader == "" {
|
||
return
|
||
}
|
||
|
||
if bearerTokenPattern.MatchString(authHeader) {
|
||
redacted := bearerTokenPattern.ReplaceAllString(authHeader, `${1}[REDACTED]`)
|
||
c.Set(RedactedAuthHeaderKey, redacted)
|
||
} else {
|
||
// 对于非 Bearer 的 token,全部脱敏
|
||
c.Set(RedactedAuthHeaderKey, "[REDACTED]")
|
||
}
|
||
|
||
// 同时处理其他敏感 Header
|
||
sensitiveHeaders := []string{"X-Api-Key", "X-Goog-Api-Key", "Api-Key"}
|
||
for _, header := range sensitiveHeaders {
|
||
if value := c.GetHeader(header); value != "" {
|
||
c.Set("redacted_"+header, "[REDACTED]")
|
||
}
|
||
}
|
||
}
|
||
|
||
// redactQueryParams 脱敏 URL 查询参数
|
||
func redactQueryParams(c *gin.Context) {
|
||
rawQuery := c.Request.URL.RawQuery
|
||
if rawQuery == "" {
|
||
return
|
||
}
|
||
|
||
redacted := queryKeyPattern.ReplaceAllString(rawQuery, `${1}[REDACTED]`)
|
||
if redacted != rawQuery {
|
||
c.Set("redactedQuery", redacted)
|
||
}
|
||
}
|
||
|
||
// LogrusLogger Gin 请求日志中间件(使用 Logrus)
|
||
func LogrusLogger(logger *logrus.Logger) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
start := time.Now()
|
||
path := c.Request.URL.Path
|
||
method := c.Request.Method
|
||
|
||
// 处理请求
|
||
c.Next()
|
||
|
||
// 计算延迟
|
||
latency := time.Since(start)
|
||
statusCode := c.Writer.Status()
|
||
clientIP := c.ClientIP()
|
||
|
||
// 构建日志字段
|
||
fields := logrus.Fields{
|
||
"status": statusCode,
|
||
"latency_ms": latency.Milliseconds(),
|
||
"ip": clientIP,
|
||
"method": method,
|
||
"path": path,
|
||
}
|
||
|
||
// 添加请求 ID(如果存在)
|
||
if requestID := getRequestID(c); requestID != "" {
|
||
fields["request_id"] = requestID
|
||
}
|
||
|
||
// 添加脱敏后的数据
|
||
if redactedBody, exists := c.Get(RedactedBodyKey); exists {
|
||
fields["body"] = redactedBody
|
||
}
|
||
|
||
if redactedAuth, exists := c.Get(RedactedAuthHeaderKey); exists {
|
||
fields["authorization"] = redactedAuth
|
||
}
|
||
|
||
if redactedQuery, exists := c.Get("redactedQuery"); exists {
|
||
fields["query"] = redactedQuery
|
||
}
|
||
|
||
// 添加用户信息(如果已认证)
|
||
if user := getAuthenticatedUser(c); user != "" {
|
||
fields["user"] = user
|
||
}
|
||
|
||
// 根据状态码选择日志级别
|
||
entry := logger.WithFields(fields)
|
||
|
||
if len(c.Errors) > 0 {
|
||
fields["errors"] = c.Errors.String()
|
||
entry.Error("Request failed")
|
||
} else {
|
||
switch {
|
||
case statusCode >= 500:
|
||
entry.Error("Server error")
|
||
case statusCode >= 400:
|
||
entry.Warn("Client error")
|
||
case statusCode >= 300:
|
||
entry.Info("Redirect")
|
||
default:
|
||
// 只在 Debug 模式记录成功请求
|
||
if logger.Level >= logrus.DebugLevel {
|
||
entry.Debug("Request completed")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// getRequestID 获取请求 ID
|
||
func getRequestID(c *gin.Context) string {
|
||
if id, exists := c.Get("request_id"); exists {
|
||
if requestID, ok := id.(string); ok {
|
||
return requestID
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// getAuthenticatedUser 获取已认证用户标识
|
||
func getAuthenticatedUser(c *gin.Context) string {
|
||
// 尝试从不同来源获取用户信息
|
||
if user, exists := c.Get("adminUser"); exists {
|
||
if authToken, ok := user.(interface{ GetID() string }); ok {
|
||
return authToken.GetID()
|
||
}
|
||
}
|
||
|
||
if user, exists := c.Get("authToken"); exists {
|
||
if authToken, ok := user.(interface{ GetID() string }); ok {
|
||
return authToken.GetID()
|
||
}
|
||
}
|
||
|
||
return ""
|
||
}
|