yunzer_go/server/middleware/operationLog.go

210 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package middleware
import (
"fmt"
"io"
"server/models"
"server/services"
"strconv"
"strings"
"time"
"github.com/beego/beego/v2/server/web/context"
)
// OperationLogMiddleware 操作日志中间件 - 记录所有的CREATE、UPDATE、DELETE操作
func OperationLogMiddleware(ctx *context.Context) {
// 记录所有重要操作包括修改类POST/PUT/PATCH/DELETE和读取类GET用于统计账户访问功能
method := ctx.Input.Method()
// 获取用户信息和租户信息(由 JWT 中间件设置在 Input.Data 中)
userId := 0
tenantId := 0
username := ""
if v := ctx.Input.GetData("userId"); v != nil {
if id, ok := v.(int); ok {
userId = id
}
}
if v := ctx.Input.GetData("tenantId"); v != nil {
if id, ok := v.(int); ok {
tenantId = id
}
}
if v := ctx.Input.GetData("username"); v != nil {
if s, ok := v.(string); ok {
username = s
}
}
// 如果无法获取用户ID继续记录为匿名访问userId=0以便统计未登录或授权失败的访问
if userId == 0 {
// debug: 输出一些上下文信息,帮助定位为何未能获取 userId
fmt.Printf("OperationLogMiddleware: anonymous request %s %s, Authorization header length=%d\n", method, ctx.Input.URL(), len(ctx.Input.Header("Authorization")))
// 确保 username 有值,便于区分
if username == "" {
username = "anonymous"
}
}
// 读取请求体(对于有请求体的方法)
var requestBody string
if method == "POST" || method == "PUT" || method == "PATCH" {
body, err := io.ReadAll(ctx.Request.Body)
if err == nil {
requestBody = string(body)
// 重置请求体,使其可以被后续处理
ctx.Request.Body = io.NopCloser(strings.NewReader(requestBody))
}
}
startTime := time.Now()
// 使用延迟函数来记录操作
defer func() {
duration := time.Since(startTime)
// 解析操作类型
operation := parseOperationType(method, ctx.Input.URL())
resourceType := parseResourceType(ctx.Input.URL())
resourceId := parseResourceId(ctx.Input.URL())
module := parseModule(ctx.Input.URL())
// 如果是读取/访问行为写入访问日志sys_access_log否则写入操作日志sys_operation_log
if operation == "READ" {
access := &models.AccessLog{
TenantId: tenantId,
UserId: userId,
Username: username,
Module: module,
ResourceType: resourceType,
ResourceId: &resourceId,
RequestUrl: ctx.Input.URL(),
IpAddress: ctx.Input.IP(),
UserAgent: ctx.Input.Header("User-Agent"),
RequestMethod: method,
Duration: int(duration.Milliseconds()),
}
// 在 QueryString 中记录查询参数和匿名标识
qs := ctx.Request.URL.RawQuery
if qs != "" {
access.QueryString = qs
}
if userId == 0 {
// 将匿名标识拼入 QueryString 以便查询(也可改为独立字段)
if access.QueryString != "" {
access.QueryString = "anonymous=true; " + access.QueryString
} else {
access.QueryString = "anonymous=true"
}
}
if err := services.AddAccessLog(access); err != nil {
fmt.Printf("Failed to save access log: %v\n", err)
}
return
}
// 创建操作日志
log := &models.OperationLog{
TenantId: tenantId,
UserId: userId,
Username: username,
Module: module,
ResourceType: resourceType,
ResourceId: &resourceId,
Operation: operation,
IpAddress: ctx.Input.IP(),
UserAgent: ctx.Input.Header("User-Agent"),
RequestMethod: method,
RequestUrl: ctx.Input.URL(),
Status: 1, // 默认成功,实际应该根据响应状态码更新
Duration: int(duration.Milliseconds()),
CreateTime: time.Now(),
}
// 如果是写操作保存请求体作为新值对于读取操作可以在Description里记录query
if requestBody != "" {
log.NewValue = requestBody
} else if method == "GET" {
// 把查询字符串放到描述里,便于分析访问参数
qs := ctx.Request.URL.RawQuery
if qs != "" {
log.Description = "query=" + qs
}
}
// 标记匿名访问信息(当 userId==0
if userId == 0 {
if log.Description != "" {
log.Description = "anonymous=true; " + log.Description
} else {
log.Description = "anonymous=true"
}
}
// 调用服务层保存日志
if err := services.AddOperationLog(log); err != nil {
fmt.Printf("Failed to save operation log: %v\n", err)
}
}()
}
// parseOperationType 根据HTTP方法解析操作类型
func parseOperationType(method, url string) string {
switch method {
case "POST":
// 检查URL是否包含特定的操作关键字
if strings.Contains(url, "login") {
return "LOGIN"
}
if strings.Contains(url, "logout") {
return "LOGOUT"
}
if strings.Contains(url, "add") || strings.Contains(url, "create") {
return "CREATE"
}
return "CREATE"
case "PUT", "PATCH":
return "UPDATE"
case "DELETE":
return "DELETE"
default:
return "READ"
}
}
// parseResourceType 根据URL解析资源类型
func parseResourceType(url string) string {
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) > 0 {
// 移除复数形式的s
resourceType := strings.TrimSuffix(parts[0], "s")
return resourceType
}
return "unknown"
}
// parseResourceId 从URL中提取资源ID
func parseResourceId(url string) int {
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) >= 2 {
// 尝试解析第二个部分为ID
if id, err := strconv.Atoi(parts[1]); err == nil {
return id
}
}
return 0
}
// parseModule 根据URL解析模块名称
func parseModule(url string) string {
// 返回与 sys_operation_log.module 字段匹配的短code例如 dict、user 等)
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) > 0 {
return strings.ToLower(parts[0])
}
return "unknown"
}