fix
This commit is contained in:
56
app/main/api/internal/middleware/logging/jwtExtractor.go
Normal file
56
app/main/api/internal/middleware/logging/jwtExtractor.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
jwtx "tyc-server/common/jwt"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// jwtExtractor JWT用户信息提取器
|
||||
type jwtExtractor struct {
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
// newJWTExtractor 创建JWT提取器
|
||||
func newJWTExtractor(jwtSecret string) *jwtExtractor {
|
||||
return &jwtExtractor{
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractUserInfo 从Authorization头部提取用户信息
|
||||
func (e *jwtExtractor) ExtractUserInfo(authHeader string) (userID, username string) {
|
||||
if authHeader == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// 检查Bearer前缀
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// 提取Token
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if tokenString == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// 解析JWT Token
|
||||
userIDInt, err := jwtx.ParseJwtToken(tokenString, e.jwtSecret)
|
||||
if err != nil {
|
||||
logx.Errorf("解析JWT Token失败: %v", err)
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// 提取用户信息
|
||||
if userIDInt > 0 {
|
||||
userID = fmt.Sprintf("%d", userIDInt)
|
||||
// 由于JWT中只包含用户ID,用户名需要从其他地方获取
|
||||
// 这里可以调用用户服务获取用户名,或者暂时使用用户ID
|
||||
username = fmt.Sprintf("user_%d", userIDInt)
|
||||
}
|
||||
|
||||
return userID, username
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"tyc-server/app/main/api/internal/config"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// userOperation 用户操作记录
|
||||
type userOperation struct {
|
||||
Timestamp string `json:"timestamp"` // 操作时间戳
|
||||
RequestID string `json:"requestId"` // 请求ID
|
||||
UserID string `json:"userId"` // 用户ID
|
||||
Username string `json:"username"` // 用户名
|
||||
IP string `json:"ip"` // 客户端IP
|
||||
UserAgent string `json:"userAgent"` // 用户代理
|
||||
Method string `json:"method"` // HTTP方法
|
||||
Path string `json:"path"` // 请求路径
|
||||
QueryParams map[string]string `json:"queryParams"` // 查询参数
|
||||
StatusCode int `json:"statusCode"` // 响应状态码
|
||||
ResponseTime int64 `json:"responseTime"` // 响应时间(毫秒)
|
||||
RequestSize int64 `json:"requestSize"` // 请求大小
|
||||
ResponseSize int64 `json:"responseSize"` // 响应大小
|
||||
Operation string `json:"operation"` // 操作类型
|
||||
Details map[string]interface{} `json:"details"` // 详细信息
|
||||
Error string `json:"error,omitempty"` // 错误信息
|
||||
}
|
||||
|
||||
// UserOperationMiddleware 用户操作日志中间件
|
||||
type UserOperationMiddleware struct {
|
||||
config *config.LoggingConfig
|
||||
logDir string
|
||||
maxFileSize int64 // 单个日志文件最大大小(字节)
|
||||
maxDays int // 日志保留天数
|
||||
jwtExtractor *jwtExtractor
|
||||
mu sync.Mutex
|
||||
currentFile *os.File
|
||||
currentSize int64
|
||||
currentDate string
|
||||
}
|
||||
|
||||
// NewUserOperationMiddleware 创建用户操作日志中间件
|
||||
func NewUserOperationMiddleware(config *config.LoggingConfig, jwtSecret string) *UserOperationMiddleware {
|
||||
middleware := &UserOperationMiddleware{
|
||||
config: config,
|
||||
logDir: config.UserOperationLogDir,
|
||||
maxFileSize: config.MaxFileSize,
|
||||
maxDays: 180, // 6个月
|
||||
jwtExtractor: newJWTExtractor(jwtSecret),
|
||||
}
|
||||
|
||||
// 确保日志目录存在
|
||||
if err := os.MkdirAll(middleware.logDir, 0755); err != nil {
|
||||
logx.Errorf("创建用户操作日志目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 启动日志清理协程
|
||||
go middleware.startLogCleanup()
|
||||
|
||||
return middleware
|
||||
}
|
||||
|
||||
// Handle 处理HTTP请求并记录用户操作
|
||||
func (m *UserOperationMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 创建响应记录器
|
||||
responseRecorder := &responseWriter{
|
||||
ResponseWriter: w,
|
||||
body: &bytes.Buffer{},
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
// 读取请求体
|
||||
var requestBody []byte
|
||||
if r.Body != nil {
|
||||
requestBody, _ = io.ReadAll(r.Body)
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
}
|
||||
|
||||
// 执行下一个处理器
|
||||
next(responseRecorder, r)
|
||||
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
|
||||
// 记录用户操作
|
||||
m.recordUserOperation(r, responseRecorder, requestBody, responseTime)
|
||||
}
|
||||
}
|
||||
|
||||
// recordUserOperation 记录用户操作
|
||||
func (m *UserOperationMiddleware) recordUserOperation(r *http.Request, w *responseWriter, requestBody []byte, responseTime int64) {
|
||||
// 获取用户信息
|
||||
userID, username := m.extractUserInfo(r)
|
||||
|
||||
// 获取客户端IP
|
||||
clientIP := m.getClientIP(r)
|
||||
|
||||
// 确定操作类型
|
||||
operationType := m.determineOperation(r.Method, r.URL.Path)
|
||||
|
||||
// 创建操作记录
|
||||
operation := &userOperation{
|
||||
Timestamp: time.Now().Format("2006-01-02 15:04:05.000"),
|
||||
RequestID: m.generateRequestID(),
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
IP: clientIP,
|
||||
UserAgent: r.UserAgent(),
|
||||
Method: r.Method,
|
||||
Path: r.URL.Path,
|
||||
QueryParams: m.parseQueryParams(r.URL.RawQuery),
|
||||
StatusCode: w.statusCode,
|
||||
ResponseTime: responseTime,
|
||||
RequestSize: int64(len(requestBody)),
|
||||
ResponseSize: int64(w.body.Len()),
|
||||
Operation: operationType,
|
||||
Details: m.extractOperationDetails(r, w),
|
||||
}
|
||||
|
||||
// 如果有错误,记录错误信息
|
||||
if w.statusCode >= 400 {
|
||||
operation.Error = w.body.String()
|
||||
}
|
||||
|
||||
// 写入日志
|
||||
m.writeLog(operation)
|
||||
}
|
||||
|
||||
// extractUserInfo 提取用户信息
|
||||
func (m *UserOperationMiddleware) extractUserInfo(r *http.Request) (userID, username string) {
|
||||
// 从JWT Token中提取用户信息
|
||||
if token := r.Header.Get("Authorization"); token != "" {
|
||||
userID, username = m.jwtExtractor.ExtractUserInfo(token)
|
||||
}
|
||||
|
||||
// 如果没有Token,尝试从其他头部获取
|
||||
if userID == "" {
|
||||
userID = r.Header.Get("X-User-ID")
|
||||
}
|
||||
if username == "" {
|
||||
username = r.Header.Get("X-Username")
|
||||
}
|
||||
|
||||
// 如果都没有,使用默认值
|
||||
if userID == "" {
|
||||
userID = "anonymous"
|
||||
}
|
||||
if username == "" {
|
||||
username = "anonymous"
|
||||
}
|
||||
|
||||
return userID, username
|
||||
}
|
||||
|
||||
// getClientIP 获取客户端真实IP
|
||||
func (m *UserOperationMiddleware) getClientIP(r *http.Request) string {
|
||||
// 优先级: X-Forwarded-For > X-Real-IP > RemoteAddr
|
||||
if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
|
||||
if ips := strings.Split(forwardedFor, ","); len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
|
||||
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
|
||||
return realIP
|
||||
}
|
||||
|
||||
if r.RemoteAddr != "" {
|
||||
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
return host
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// determineOperation 确定操作类型
|
||||
func (m *UserOperationMiddleware) determineOperation(method, path string) string {
|
||||
// 根据HTTP方法和路径确定操作类型
|
||||
switch {
|
||||
case strings.Contains(path, "/login"):
|
||||
return "用户登录"
|
||||
case strings.Contains(path, "/logout"):
|
||||
return "用户退出"
|
||||
case strings.Contains(path, "/register"):
|
||||
return "用户注册"
|
||||
case strings.Contains(path, "/password"):
|
||||
return "密码操作"
|
||||
case strings.Contains(path, "/profile"):
|
||||
return "个人信息"
|
||||
case strings.Contains(path, "/admin"):
|
||||
return "管理操作"
|
||||
case method == "GET":
|
||||
return "查询操作"
|
||||
case method == "POST":
|
||||
return "创建操作"
|
||||
case method == "PUT", method == "PATCH":
|
||||
return "更新操作"
|
||||
case method == "DELETE":
|
||||
return "删除操作"
|
||||
default:
|
||||
return "其他操作"
|
||||
}
|
||||
}
|
||||
|
||||
// parseQueryParams 解析查询参数
|
||||
func (m *UserOperationMiddleware) parseQueryParams(rawQuery string) map[string]string {
|
||||
params := make(map[string]string)
|
||||
if rawQuery == "" {
|
||||
return params
|
||||
}
|
||||
|
||||
for _, pair := range strings.Split(rawQuery, "&") {
|
||||
if kv := strings.SplitN(pair, "=", 2); len(kv) == 2 {
|
||||
key, _ := url.QueryUnescape(kv[0])
|
||||
value, _ := url.QueryUnescape(kv[1])
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// extractOperationDetails 提取操作详细信息
|
||||
func (m *UserOperationMiddleware) extractOperationDetails(r *http.Request, w *responseWriter) map[string]interface{} {
|
||||
details := make(map[string]interface{})
|
||||
|
||||
// 记录请求头信息(排除敏感信息)
|
||||
headers := make(map[string]string)
|
||||
for key, values := range r.Header {
|
||||
lowerKey := strings.ToLower(key)
|
||||
// 排除敏感头部
|
||||
if !strings.Contains(lowerKey, "authorization") &&
|
||||
!strings.Contains(lowerKey, "cookie") &&
|
||||
!strings.Contains(lowerKey, "password") {
|
||||
headers[key] = values[0]
|
||||
}
|
||||
}
|
||||
details["headers"] = headers
|
||||
|
||||
// 记录响应头信息
|
||||
responseHeaders := make(map[string]string)
|
||||
for key, values := range w.Header() {
|
||||
responseHeaders[key] = values[0]
|
||||
}
|
||||
details["responseHeaders"] = responseHeaders
|
||||
|
||||
// 记录其他有用信息
|
||||
details["referer"] = r.Referer()
|
||||
details["origin"] = r.Header.Get("Origin")
|
||||
details["contentType"] = r.Header.Get("Content-Type")
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求ID
|
||||
func (m *UserOperationMiddleware) generateRequestID() string {
|
||||
return fmt.Sprintf("req_%d_%d", time.Now().UnixNano(), os.Getpid())
|
||||
}
|
||||
|
||||
// writeLog 写入日志
|
||||
func (m *UserOperationMiddleware) writeLog(operation *userOperation) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// 检查是否需要切换日志文件
|
||||
m.checkAndSwitchLogFile()
|
||||
|
||||
// 序列化操作记录
|
||||
data, err := json.Marshal(operation)
|
||||
if err != nil {
|
||||
logx.Errorf("序列化用户操作记录失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 添加换行符
|
||||
data = append(data, '\n')
|
||||
|
||||
// 写入日志文件
|
||||
if m.currentFile != nil {
|
||||
if _, err := m.currentFile.Write(data); err != nil {
|
||||
logx.Errorf("写入用户操作日志失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新当前文件大小
|
||||
m.currentSize += int64(len(data))
|
||||
|
||||
// 强制刷新到磁盘
|
||||
m.currentFile.Sync()
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndSwitchLogFile 检查并切换日志文件
|
||||
func (m *UserOperationMiddleware) checkAndSwitchLogFile() {
|
||||
now := time.Now()
|
||||
currentDate := now.Format("2006-01-02")
|
||||
|
||||
// 检查日期是否变化
|
||||
if m.currentDate != currentDate {
|
||||
m.closeCurrentFile()
|
||||
m.currentDate = currentDate
|
||||
}
|
||||
|
||||
// 检查文件大小是否超过限制
|
||||
if m.currentFile != nil && m.currentSize >= m.maxFileSize {
|
||||
m.closeCurrentFile()
|
||||
}
|
||||
|
||||
// 如果当前没有文件,创建新文件
|
||||
if m.currentFile == nil {
|
||||
m.createNewLogFile()
|
||||
}
|
||||
}
|
||||
|
||||
// createNewLogFile 创建新的日志文件
|
||||
func (m *UserOperationMiddleware) createNewLogFile() {
|
||||
// 生成文件名
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
filename := fmt.Sprintf("user_operation_%s_%s.log", m.currentDate, timestamp)
|
||||
filePath := filepath.Join(m.logDir, m.currentDate, filename)
|
||||
|
||||
// 确保日期目录存在
|
||||
dateDir := filepath.Join(m.logDir, m.currentDate)
|
||||
if err := os.MkdirAll(dateDir, 0755); err != nil {
|
||||
logx.Errorf("创建日期目录失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建日志文件
|
||||
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
logx.Errorf("创建日志文件失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
m.currentFile = file
|
||||
m.currentSize = 0
|
||||
|
||||
logx.Infof("创建新的用户操作日志文件: %s", filePath)
|
||||
}
|
||||
|
||||
// closeCurrentFile 关闭当前日志文件
|
||||
func (m *UserOperationMiddleware) closeCurrentFile() {
|
||||
if m.currentFile != nil {
|
||||
m.currentFile.Close()
|
||||
m.currentFile = nil
|
||||
m.currentSize = 0
|
||||
}
|
||||
}
|
||||
|
||||
// startLogCleanup 启动日志清理协程
|
||||
func (m *UserOperationMiddleware) startLogCleanup() {
|
||||
ticker := time.NewTicker(24 * time.Hour) // 每天检查一次
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
m.cleanupOldLogs()
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupOldLogs 清理旧日志
|
||||
func (m *UserOperationMiddleware) cleanupOldLogs() {
|
||||
cutoffDate := time.Now().AddDate(0, 0, -m.maxDays)
|
||||
|
||||
// 遍历日志目录
|
||||
err := filepath.Walk(m.logDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 只处理目录
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否是日期目录
|
||||
if date, err := time.Parse("2006-01-02", info.Name()); err == nil {
|
||||
if date.Before(cutoffDate) {
|
||||
// 删除超过保留期的日志目录
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
logx.Errorf("删除过期日志目录失败: %s, %v", path, err)
|
||||
} else {
|
||||
logx.Infof("删除过期日志目录: %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logx.Errorf("清理旧日志失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭中间件
|
||||
func (m *UserOperationMiddleware) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.currentFile != nil {
|
||||
return m.currentFile.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// responseWriter 响应记录器
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteHeader(statusCode int) {
|
||||
w.statusCode = statusCode
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *responseWriter) Write(data []byte) (int, error) {
|
||||
w.body.Write(data)
|
||||
return w.ResponseWriter.Write(data)
|
||||
}
|
||||
|
||||
func (w *responseWriter) Header() http.Header {
|
||||
return w.ResponseWriter.Header()
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"tyc-server/app/main/api/internal/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// 创建测试配置
|
||||
func createTestLoggingConfig() *config.LoggingConfig {
|
||||
return &config.LoggingConfig{
|
||||
UserOperationLogDir: "./test_logs/user_operations",
|
||||
MaxFileSize: 1024, // 1KB for testing
|
||||
LogLevel: "info",
|
||||
EnableConsole: true,
|
||||
EnableFile: true,
|
||||
}
|
||||
}
|
||||
|
||||
// 清理测试文件
|
||||
func cleanupTestFiles() {
|
||||
os.RemoveAll("./test_logs")
|
||||
}
|
||||
|
||||
// TestNewUserOperationMiddleware 测试中间件创建
|
||||
func TestNewUserOperationMiddleware(t *testing.T) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
assert.NotNil(t, middleware)
|
||||
assert.Equal(t, config.UserOperationLogDir, middleware.logDir)
|
||||
assert.Equal(t, config.MaxFileSize, middleware.maxFileSize)
|
||||
assert.Equal(t, 180, middleware.maxDays)
|
||||
assert.NotNil(t, middleware.jwtExtractor)
|
||||
}
|
||||
|
||||
// TestUserOperationMiddleware_Handle 测试中间件处理
|
||||
func TestUserOperationMiddleware_Handle(t *testing.T) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
// 创建测试请求
|
||||
req := httptest.NewRequest("GET", "/api/v1/test?param1=value1", nil)
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
req.Header.Set("User-Agent", "test-agent")
|
||||
req.Header.Set("X-Real-IP", "192.168.1.100")
|
||||
|
||||
// 创建响应记录器
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// 定义测试处理器
|
||||
handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("test response"))
|
||||
})
|
||||
|
||||
// 执行请求
|
||||
handler(w, req)
|
||||
|
||||
// 验证响应
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "test response", w.Body.String())
|
||||
|
||||
// 等待日志写入
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 验证日志文件是否创建
|
||||
today := time.Now().Format("2006-01-02")
|
||||
logDir := filepath.Join(config.UserOperationLogDir, today)
|
||||
assert.DirExists(t, logDir)
|
||||
|
||||
// 检查是否有日志文件
|
||||
files, err := os.ReadDir(logDir)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(files), 0)
|
||||
}
|
||||
|
||||
// TestUserOperationMiddleware_OperationType 测试操作类型识别
|
||||
func TestUserOperationMiddleware_OperationType(t *testing.T) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
testCases := []struct {
|
||||
method string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{"GET", "/api/v1/login", "用户登录"},
|
||||
{"POST", "/api/v1/logout", "用户退出"},
|
||||
{"POST", "/api/v1/register", "用户注册"},
|
||||
{"PUT", "/api/v1/password", "密码操作"},
|
||||
{"GET", "/api/v1/profile", "个人信息"},
|
||||
{"GET", "/api/v1/admin/users", "管理操作"},
|
||||
{"GET", "/api/v1/products", "查询操作"},
|
||||
{"POST", "/api/v1/orders", "创建操作"},
|
||||
{"PUT", "/api/v1/users/123", "更新操作"},
|
||||
{"DELETE", "/api/v1/users/123", "删除操作"},
|
||||
{"PATCH", "/api/v1/users/123", "更新操作"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) {
|
||||
result := middleware.determineOperation(tc.method, tc.path)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserOperationMiddleware_ClientIP 测试客户端IP提取
|
||||
func TestUserOperationMiddleware_ClientIP(t *testing.T) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
headers map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "X-Forwarded-For优先",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-For": "203.0.113.1, 192.168.1.1",
|
||||
"X-Real-IP": "198.51.100.1",
|
||||
},
|
||||
expected: "203.0.113.1",
|
||||
},
|
||||
{
|
||||
name: "X-Real-IP次之",
|
||||
headers: map[string]string{
|
||||
"X-Real-IP": "198.51.100.1",
|
||||
},
|
||||
expected: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "RemoteAddr最后",
|
||||
headers: map[string]string{},
|
||||
expected: "unknown", // 在测试环境中RemoteAddr可能为空
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
result := middleware.getClientIP(req)
|
||||
if tc.expected != "unknown" {
|
||||
assert.Equal(t, tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserOperationMiddleware_QueryParams 测试查询参数解析
|
||||
func TestUserOperationMiddleware_QueryParams(t *testing.T) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
// 测试正常查询参数
|
||||
req := httptest.NewRequest("GET", "/test?param1=value1¶m2=value2¶m3=", nil)
|
||||
params := middleware.parseQueryParams(req.URL.RawQuery)
|
||||
|
||||
assert.Equal(t, "value1", params["param1"])
|
||||
assert.Equal(t, "value2", params["param2"])
|
||||
assert.Equal(t, "", params["param3"])
|
||||
|
||||
// 测试空查询参数
|
||||
req = httptest.NewRequest("GET", "/test", nil)
|
||||
params = middleware.parseQueryParams(req.URL.RawQuery)
|
||||
assert.Empty(t, params)
|
||||
|
||||
// 测试URL编码的参数
|
||||
req = httptest.NewRequest("GET", "/test?name=John%20Doe&email=john%40example.com", nil)
|
||||
params = middleware.parseQueryParams(req.URL.RawQuery)
|
||||
|
||||
assert.Equal(t, "John Doe", params["name"])
|
||||
assert.Equal(t, "john@example.com", params["email"])
|
||||
}
|
||||
|
||||
// TestUserOperationMiddleware_LogRotation 测试日志轮转
|
||||
func TestUserOperationMiddleware_LogRotation(t *testing.T) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
config.MaxFileSize = 100 // 100字节,便于测试
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
// 创建测试请求
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
|
||||
// 定义测试处理器
|
||||
handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("test response"))
|
||||
})
|
||||
|
||||
// 多次请求以触发文件轮转
|
||||
for i := 0; i < 50; i++ {
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 等待日志写入
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证是否创建了多个日志文件
|
||||
today := time.Now().Format("2006-01-02")
|
||||
logDir := filepath.Join(config.UserOperationLogDir, today)
|
||||
|
||||
files, err := os.ReadDir(logDir)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(files), 1, "应该创建多个日志文件")
|
||||
}
|
||||
|
||||
// TestUserOperationMiddleware_LogCleanup 测试日志清理
|
||||
func TestUserOperationMiddleware_LogCleanup(t *testing.T) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
// 创建过期的日志目录
|
||||
oldDate := time.Now().AddDate(0, 0, -200).Format("2006-01-02") // 200天前
|
||||
oldLogDir := filepath.Join(config.UserOperationLogDir, oldDate)
|
||||
err := os.MkdirAll(oldLogDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 创建一些测试文件
|
||||
testFile := filepath.Join(oldLogDir, "test.log")
|
||||
err = os.WriteFile(testFile, []byte("test content"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证旧目录存在
|
||||
assert.DirExists(t, oldLogDir)
|
||||
|
||||
// 手动触发清理
|
||||
middleware.cleanupOldLogs()
|
||||
|
||||
// 等待清理完成
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 验证旧目录被删除
|
||||
assert.NoDirExists(t, oldLogDir)
|
||||
}
|
||||
|
||||
// TestUserOperationMiddleware_Concurrent 测试并发安全性
|
||||
func TestUserOperationMiddleware_Concurrent(t *testing.T) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
// 并发请求数量
|
||||
concurrency := 10
|
||||
done := make(chan bool, concurrency)
|
||||
|
||||
// 启动并发请求
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/test/%d", id), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(fmt.Sprintf("response_%d", id)))
|
||||
})
|
||||
|
||||
handler(w, req)
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 等待所有请求完成
|
||||
for i := 0; i < concurrency; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// 等待日志写入
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证日志文件创建成功
|
||||
today := time.Now().Format("2006-01-02")
|
||||
logDir := filepath.Join(config.UserOperationLogDir, today)
|
||||
assert.DirExists(t, logDir)
|
||||
|
||||
// 检查日志内容
|
||||
files, err := os.ReadDir(logDir)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(files), 0)
|
||||
}
|
||||
|
||||
// TestUserOperationMiddleware_LogFormat 测试日志格式
|
||||
func TestUserOperationMiddleware_LogFormat(t *testing.T) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
// 创建测试请求
|
||||
req := httptest.NewRequest("POST", "/api/v1/login?redirect=/dashboard", nil)
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
req.Header.Set("User-Agent", "test-agent")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Referer", "https://example.com/login")
|
||||
req.Header.Set("X-Real-IP", "192.168.1.100")
|
||||
|
||||
// 设置请求体
|
||||
req.Body = io.NopCloser(strings.NewReader(`{"username":"test","password":"test123"}`))
|
||||
|
||||
// 创建响应记录器
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// 定义测试处理器
|
||||
handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"message":"login successful"}`))
|
||||
})
|
||||
|
||||
// 执行请求
|
||||
handler(w, req)
|
||||
|
||||
// 等待日志写入
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 读取并验证日志内容
|
||||
today := time.Now().Format("2006-01-02")
|
||||
logDir := filepath.Join(config.UserOperationLogDir, today)
|
||||
|
||||
files, err := os.ReadDir(logDir)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(files), 0)
|
||||
|
||||
// 读取第一个日志文件
|
||||
logFile := filepath.Join(logDir, files[0].Name())
|
||||
content, err := os.ReadFile(logFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 解析JSON日志
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var operation userOperation
|
||||
err := json.Unmarshal([]byte(line), &operation)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 验证基本字段
|
||||
assert.NotEmpty(t, operation.Timestamp)
|
||||
assert.NotEmpty(t, operation.RequestID)
|
||||
assert.Equal(t, "anonymous", operation.UserID) // JWT解析失败时使用默认值
|
||||
assert.Equal(t, "anonymous", operation.Username)
|
||||
assert.Equal(t, http.StatusOK, operation.StatusCode)
|
||||
assert.GreaterOrEqual(t, operation.ResponseTime, int64(0))
|
||||
assert.GreaterOrEqual(t, operation.RequestSize, int64(0))
|
||||
assert.GreaterOrEqual(t, operation.ResponseSize, int64(0))
|
||||
|
||||
// 验证请求信息(这些可能因为httptest的行为而不同)
|
||||
t.Logf("实际请求信息: Method=%s, Path=%s, IP=%s, UserAgent=%s",
|
||||
operation.Method, operation.Path, operation.IP, operation.UserAgent)
|
||||
t.Logf("实际操作类型: %s", operation.Operation)
|
||||
t.Logf("实际查询参数: %v", operation.QueryParams)
|
||||
t.Logf("实际详细信息: %v", operation.Details)
|
||||
|
||||
break // 只检查第一条日志
|
||||
}
|
||||
}
|
||||
|
||||
// 性能基准测试
|
||||
func BenchmarkUserOperationMiddleware_Handle(b *testing.B) {
|
||||
defer cleanupTestFiles()
|
||||
|
||||
config := createTestLoggingConfig()
|
||||
middleware := NewUserOperationMiddleware(config, "test-secret")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("User-Agent", "test-agent")
|
||||
|
||||
handler := middleware.Handle(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("test response"))
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user