All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
186 lines
4.9 KiB
Go
186 lines
4.9 KiB
Go
package logger
|
||
|
||
import (
|
||
"context"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/bytedance/sonic"
|
||
"github.com/gofiber/fiber/v2"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
const (
|
||
// MaxBodyLogSize 限制记录的请求/响应 body 大小为 50KB
|
||
MaxBodyLogSize = 50 * 1024
|
||
)
|
||
|
||
// truncateBody 截断 body 到指定大小
|
||
func truncateBody(body []byte, maxSize int) string {
|
||
if len(body) == 0 {
|
||
return ""
|
||
}
|
||
|
||
if len(body) <= maxSize {
|
||
return string(body)
|
||
}
|
||
|
||
// 超过限制,截断并添加提示
|
||
return string(body[:maxSize]) + "... (truncated)"
|
||
}
|
||
|
||
// maskSensitiveValue 按字段名判断并脱敏访问日志中的敏感值
|
||
func maskSensitiveValue(key, value string) string {
|
||
if value == "" {
|
||
return value
|
||
}
|
||
normalized := strings.ToLower(key)
|
||
if strings.Contains(normalized, "password") ||
|
||
strings.Contains(normalized, "sign") ||
|
||
strings.Contains(normalized, "nonce") ||
|
||
strings.Contains(normalized, "token") ||
|
||
strings.Contains(normalized, "secret") {
|
||
return "***"
|
||
}
|
||
return value
|
||
}
|
||
|
||
// sanitizeQuery 脱敏 query 中的密码、签名、nonce、token 等字段
|
||
func sanitizeQuery(rawQuery string) string {
|
||
if rawQuery == "" {
|
||
return ""
|
||
}
|
||
values, err := url.ParseQuery(rawQuery)
|
||
if err != nil {
|
||
return rawQuery
|
||
}
|
||
for key, items := range values {
|
||
for index, item := range items {
|
||
items[index] = maskSensitiveValue(key, item)
|
||
}
|
||
values[key] = items
|
||
}
|
||
return values.Encode()
|
||
}
|
||
|
||
// sanitizeBody 脱敏 JSON 请求体后再写入访问日志
|
||
func sanitizeBody(rawBody []byte) string {
|
||
if len(rawBody) == 0 {
|
||
return ""
|
||
}
|
||
var payload any
|
||
if err := sonic.Unmarshal(rawBody, &payload); err != nil {
|
||
return truncateBody(rawBody, MaxBodyLogSize)
|
||
}
|
||
sanitizeJSONValue(payload)
|
||
data, err := sonic.Marshal(payload)
|
||
if err != nil {
|
||
return truncateBody(rawBody, MaxBodyLogSize)
|
||
}
|
||
return truncateBody(data, MaxBodyLogSize)
|
||
}
|
||
|
||
// sanitizeJSONValue 递归脱敏 JSON 对象中的敏感字段
|
||
func sanitizeJSONValue(value any) {
|
||
switch typed := value.(type) {
|
||
case map[string]any:
|
||
for key, item := range typed {
|
||
if masked, ok := item.(string); ok {
|
||
typed[key] = maskSensitiveValue(key, masked)
|
||
continue
|
||
}
|
||
if shouldMaskField(key) {
|
||
typed[key] = "***"
|
||
continue
|
||
}
|
||
sanitizeJSONValue(item)
|
||
}
|
||
case []any:
|
||
for _, item := range typed {
|
||
sanitizeJSONValue(item)
|
||
}
|
||
}
|
||
}
|
||
|
||
// shouldMaskField 判断字段名是否属于访问日志敏感字段
|
||
func shouldMaskField(key string) bool {
|
||
normalized := strings.ToLower(key)
|
||
return strings.Contains(normalized, "password") ||
|
||
strings.Contains(normalized, "sign") ||
|
||
strings.Contains(normalized, "nonce") ||
|
||
strings.Contains(normalized, "token") ||
|
||
strings.Contains(normalized, "secret")
|
||
}
|
||
|
||
// Middleware 创建 Fiber 日志中间件
|
||
// 记录所有 HTTP 请求到访问日志(包括请求和响应 body)
|
||
func Middleware() fiber.Handler {
|
||
return func(c *fiber.Ctx) error {
|
||
// 记录请求开始时间
|
||
startTime := time.Now()
|
||
c.Locals(constants.ContextKeyStartTime, startTime)
|
||
|
||
// 注入请求上下文,供 Service 层审计日志复用
|
||
ctx := c.UserContext()
|
||
if rid := c.Locals(constants.ContextKeyRequestID); rid != nil {
|
||
if requestID, ok := rid.(string); ok && requestID != "" {
|
||
ctx = context.WithValue(ctx, constants.ContextKeyRequestID, requestID)
|
||
}
|
||
}
|
||
ctx = context.WithValue(ctx, constants.ContextKeyIP, c.IP())
|
||
ctx = context.WithValue(ctx, constants.ContextKeyUserAgent, c.Get("User-Agent"))
|
||
ctx = context.WithValue(ctx, constants.ContextKeyRequestPath, c.Path())
|
||
ctx = context.WithValue(ctx, constants.ContextKeyRequestMethod, c.Method())
|
||
c.SetUserContext(ctx)
|
||
|
||
// 获取请求 body(在 c.Next() 之前读取)
|
||
requestBody := sanitizeBody(c.Body())
|
||
|
||
// 获取 query 参数
|
||
queryParams := sanitizeQuery(string(c.Request().URI().QueryString()))
|
||
|
||
// 处理请求
|
||
err := c.Next()
|
||
|
||
// 计算请求持续时间
|
||
duration := time.Since(startTime)
|
||
|
||
// 获取请求 ID(由 requestid 中间件设置)
|
||
requestID := ""
|
||
if rid := c.Locals(constants.ContextKeyRequestID); rid != nil {
|
||
requestID = rid.(string)
|
||
}
|
||
|
||
// 获取用户 ID(由 auth 中间件设置)
|
||
var userID uint
|
||
if uid := c.Locals(constants.ContextKeyUserID); uid != nil {
|
||
if id, ok := uid.(uint); ok {
|
||
userID = id
|
||
}
|
||
}
|
||
|
||
// 获取响应 body
|
||
responseBody := truncateBody(c.Response().Body(), MaxBodyLogSize)
|
||
|
||
// 记录访问日志
|
||
accessLogger := GetAccessLogger()
|
||
accessLogger.Info("",
|
||
zap.String("method", c.Method()),
|
||
zap.String("path", c.Path()),
|
||
zap.String("query", queryParams),
|
||
zap.Int("status", c.Response().StatusCode()),
|
||
zap.Float64("duration_ms", float64(duration.Microseconds())/1000.0),
|
||
zap.String("request_id", requestID),
|
||
zap.String("ip", c.IP()),
|
||
zap.String("user_agent", c.Get("User-Agent")),
|
||
zap.Uint("user_id", userID),
|
||
zap.String("request_body", requestBody),
|
||
zap.String("response_body", responseBody),
|
||
)
|
||
|
||
return err
|
||
}
|
||
}
|