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赋值前触发,需确保操作人已注入上下文。
要点总结
- 通过回调钩子拦截
create、update、delete操作,自动记录审计日志 - 审计日志包含操作表、记录 ID、操作类型、操作人、变更前后值等信息
- 注册钩子时使用
SkipHooks: true避免审计日志写入触发递归回调 - 操作人信息通过上下文传递,需在请求入口中间件注入
- 删除操作钩子必须在
gorm:delete之前执行,确保能捕获原始数据 - 软删除场景可在模型字段中直接记录操作人,简化审计链路
存放路径:D:\git2\jwdev\articles\GORM\专家\安全与数据保护\权限与审计日志.md
📝 发现内容有误?点击此处直接编辑