169 lines
4.3 KiB
Go
169 lines
4.3 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"sundynix-micro-go/app/system/rpc/system"
|
|
"sundynix-micro-go/app/system/rpc/systemservice"
|
|
jwtUtil "sundynix-micro-go/common/utils/jwt"
|
|
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
)
|
|
|
|
// OperationLogMiddleware 操作日志中间件(异步写入 system-rpc)
|
|
type OperationLogMiddleware struct {
|
|
systemRpc systemservice.SystemService
|
|
jwtSecret string
|
|
logChan chan *system.CreateOperationRecordReq
|
|
}
|
|
|
|
func NewOperationLogMiddleware(systemRpc systemservice.SystemService, jwtSecret string) *OperationLogMiddleware {
|
|
m := &OperationLogMiddleware{
|
|
systemRpc: systemRpc,
|
|
jwtSecret: jwtSecret,
|
|
logChan: make(chan *system.CreateOperationRecordReq, 500),
|
|
}
|
|
// 启动异步消费者,避免阻塞请求
|
|
go m.consumer()
|
|
return m
|
|
}
|
|
|
|
func (m *OperationLogMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// 跳过健康检查和 OPTIONS 预检
|
|
if r.URL.Path == "/health" || r.Method == http.MethodOptions {
|
|
next(w, r)
|
|
return
|
|
}
|
|
|
|
startTime := time.Now()
|
|
|
|
// 读取并缓存请求体(限制大小,跳过文件上传)
|
|
var bodyStr string
|
|
if r.Body != nil && !strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") {
|
|
bodyBytes, _ := io.ReadAll(io.LimitReader(r.Body, 2048))
|
|
bodyStr = string(bodyBytes)
|
|
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // 恢复以供上游读取
|
|
}
|
|
|
|
clientId := r.Header.Get("X-Client-Id")
|
|
userId := m.extractUserId(r)
|
|
clientIP := getClientIP(r)
|
|
|
|
// 包装 ResponseWriter 捕获响应状态码和响应体
|
|
rw := &responseCapture{ResponseWriter: w, statusCode: http.StatusOK}
|
|
|
|
next(rw, r)
|
|
|
|
latency := time.Since(startTime)
|
|
|
|
record := &system.CreateOperationRecordReq{
|
|
ClientId: clientId,
|
|
Ip: clientIP,
|
|
Method: r.Method,
|
|
Path: r.URL.Path,
|
|
Status: int32(rw.statusCode),
|
|
Latency: latency.Nanoseconds(),
|
|
Agent: truncate(r.UserAgent(), 500),
|
|
ErrorMessage: rw.errorMsg(),
|
|
Body: truncate(bodyStr, 2000),
|
|
Resp: truncate(rw.body.String(), 2000),
|
|
UserId: userId,
|
|
}
|
|
|
|
// 异步写入,不阻塞响应
|
|
select {
|
|
case m.logChan <- record:
|
|
default:
|
|
logx.Error("[zero-gateway] 操作日志缓冲区满,丢弃日志")
|
|
}
|
|
}
|
|
}
|
|
|
|
// consumer 异步消费操作日志并通过 system-rpc 写入数据库
|
|
func (m *OperationLogMiddleware) consumer() {
|
|
for record := range m.logChan {
|
|
_, err := m.systemRpc.CreateOperationRecord(context.Background(), record)
|
|
if err != nil {
|
|
logx.Errorf("[zero-gateway] 写入操作日志失败: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// extractUserId 从 Authorization 头解析 JWT 获取 userId
|
|
func (m *OperationLogMiddleware) extractUserId(r *http.Request) string {
|
|
// 优先从鉴权中间件注入的请求头获取(避免重复解析 JWT)
|
|
if uid := r.Header.Get("X-User-Id"); uid != "" {
|
|
return uid
|
|
}
|
|
// fallback: 自己解析
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
return ""
|
|
}
|
|
tokenStr := jwtUtil.GetTokenFromHeader(authHeader)
|
|
if tokenStr == "" {
|
|
return ""
|
|
}
|
|
j := jwtUtil.NewJWT(m.jwtSecret)
|
|
claims, err := j.ParseToken(tokenStr)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return claims.BaseClaims.ID
|
|
}
|
|
|
|
// responseCapture 捕获响应状态码和响应体
|
|
type responseCapture struct {
|
|
http.ResponseWriter
|
|
statusCode int
|
|
body bytes.Buffer
|
|
}
|
|
|
|
func (rc *responseCapture) WriteHeader(code int) {
|
|
rc.statusCode = code
|
|
rc.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func (rc *responseCapture) Write(b []byte) (int, error) {
|
|
if rc.body.Len() < 2048 {
|
|
rc.body.Write(b)
|
|
}
|
|
return rc.ResponseWriter.Write(b)
|
|
}
|
|
|
|
func (rc *responseCapture) errorMsg() string {
|
|
if rc.statusCode >= 400 {
|
|
return rc.body.String()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// getClientIP 获取真实客户端 IP
|
|
func getClientIP(r *http.Request) string {
|
|
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
|
|
return strings.Split(ip, ",")[0]
|
|
}
|
|
if ip := r.Header.Get("X-Real-Ip"); ip != "" {
|
|
return ip
|
|
}
|
|
addr := r.RemoteAddr
|
|
if idx := strings.LastIndex(addr, ":"); idx != -1 {
|
|
return addr[:idx]
|
|
}
|
|
return addr
|
|
}
|
|
|
|
// truncate 截断字符串,防止写入过长内容
|
|
func truncate(s string, maxLen int) string {
|
|
if len(s) > maxLen {
|
|
return s[:maxLen] + "..."
|
|
}
|
|
return s
|
|
}
|