全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-21 8 分钟 ✍️ juanwangdev

GORM 权限与审计日志

GORM 通过回调钩子拦截数据变更操作,自动记录操作人、操作时间、操作类型等信息到审计日志表。

审计日志模型

日志表结构定义

Go
type AuditLog struct {
    ID         uint      `gorm:"primaryKey"`
    TableName  string    `gorm:"index"`       // 操作的表
    RecordID   uint      `gorm:"index"`       // 操作的记录 ID
    Action     string    `gorm:"index"`       // create, update, delete
    OperatorID uint      `gorm:"index"`       // 操作人 ID
    Operator   string    `gorm:"size:100"`    // 操作人姓名
    OldValue   string    `gorm:"type:text"`   // 变更前值(JSON)
    NewValue   string    `gorm:"type:text"`   // 变更后值(JSON)
    CreatedAt  time.Time `gorm:"index"`       // 操作时间
}

回调钩子实现

创建操作审计

Go
func RegisterAuditCallbacks(db *gorm.DB) error {
    // 创建后记录
    err := db.Callback().Create().After("gorm:create").Register("audit:create", func(db *gorm.DB) {
        if db.Error != nil {
            return
        }
        
        operatorID := GetOperatorFromContext(db.Statement.Context)
        tableName := db.Statement.Table
        recordID := db.Statement.PrimaryFields[0].Value
        
        log := AuditLog{
            TableName:  tableName,
            RecordID:   recordID.(uint),
            Action:     "create",
            OperatorID: operatorID,
            NewValue:   toJSON(db.Statement.Dest),
            CreatedAt:  time.Now(),
        }
        
        // 使用新会话避免递归
        db.Session(&gorm.Session{SkipHooks: true}).Create(&log)
    })
    return err
}

更新操作审计

Go
func RegisterAuditUpdateCallback(db *gorm.DB) error {
    return db.Callback().Update().After("gorm:update").Register("audit:update", func(db *gorm.DB) {
        if db.Error != nil {
            return
        }
        
        operatorID := GetOperatorFromContext(db.Statement.Context)
        tableName := db.Statement.Table
        
        // 获取变更字段
        updateValues := db.Statement.Dest.(map[string]interface{})
        
        log := AuditLog{
            TableName:  tableName,
            RecordID:   getPrimaryID(db),
            Action:     "update",
            OperatorID: operatorID,
            NewValue:   toJSON(updateValues),
            CreatedAt:  time.Now(),
        }
        
        db.Session(&gorm.Session{SkipHooks: true}).Create(&log)
    })
}

删除操作审计

Go
func RegisterAuditDeleteCallback(db *gorm.DB) error {
    return db.Callback().Delete().Before("gorm:delete").Register("audit:delete", func(db *gorm.DB) {
        if db.Error != nil {
            return
        }
        
        operatorID := GetOperatorFromContext(db.Statement.Context)
        tableName := db.Statement.Table
        
        // 删除前查询原始值
        var oldValue map[string]interface{}
        db.First(&oldValue)
        
        log := AuditLog{
            TableName:  tableName,
            RecordID:   getPrimaryID(db),
            Action:     "delete",
            OperatorID: operatorID,
            OldValue:   toJSON(oldValue),
            CreatedAt:  time.Now(),
        }
        
        db.Session(&gorm.Session{SkipHooks: true}).Create(&log)
    })
}

注意: 删除操作钩子必须注册在 gorm:delete 之前,确保在删除前捕获原始数据。

操作人上下文传递

中间件注入操作人

Go
func GetOperatorFromContext(ctx context.Context) uint {
    if operatorID, ok := ctx.Value("operator_id").(uint); ok {
        return operatorID
    }
    return 0 // 系统操作
}

// Gin 中间件示例
func AuditMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        userID := c.GetUint("user_id")
        ctx := context.WithValue(c.Request.Context(), "operator_id", userID)
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

审计日志查询

按条件查询审计记录

Go
func QueryAuditLogs(db *gorm.DB, tableName string, recordID uint, startTime, endTime time.Time) ([]AuditLog, error) {
    var logs []AuditLog
    query := db.Model(&AuditLog{})
    
    if tableName != "" {
        query = query.Where("table_name = ?", tableName)
    }
    if recordID > 0 {
        query = query.Where("record_id = ?", recordID)
    }
    if !startTime.IsZero() {
        query = query.Where("created_at >= ?", startTime)
    }
    if !endTime.IsZero() {
        query = query.Where("created_at <= ?", endTime)
    }
    
    err := query.Order("created_at DESC").Find(&logs).Error
    return logs, err
}

// 查询某记录的操作历史
logs, _ := QueryAuditLogs(db, "orders", 123, time.Now().AddDate(0, -1, 0), time.Now())

软删除审计

配合软删除记录操作人

Go
type Order struct {
    ID        uint
    Amount    decimal.Decimal
    DeletedAt gorm.DeletedAt `gorm:"index"`
    
    // 审计字段
    UpdatedBy uint
    DeletedBy uint
}

// 更新时记录操作人
func (o *Order) BeforeUpdate(tx *gorm.DB) error {
    o.UpdatedBy = GetOperatorFromContext(tx.Statement.Context)
    return nil
}

// 删除时记录删除人
func (o *Order) BeforeDelete(tx *gorm.DB) error {
    o.DeletedBy = GetOperatorFromContext(tx.Statement.Context)
    return nil
}

注意: 软删除场景下 BeforeDelete 钩子会在 DeletedAt 赋值前触发,需确保操作人已注入上下文。

要点总结

  • 通过回调钩子拦截 createupdatedelete 操作,自动记录审计日志
  • 审计日志包含操作表、记录 ID、操作类型、操作人、变更前后值等信息
  • 注册钩子时使用 SkipHooks: true 避免审计日志写入触发递归回调
  • 操作人信息通过上下文传递,需在请求入口中间件注入
  • 删除操作钩子必须在 gorm:delete 之前执行,确保能捕获原始数据
  • 软删除场景可在模型字段中直接记录操作人,简化审计链路

存放路径:D:\git2\jwdev\articles\GORM\专家\安全与数据保护\权限与审计日志.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 GORM 数据脱敏处理
下一篇 → gen 代码生成器使用
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库