我将会从:
- 技术角度
- 人性角度
- 安全角度
去告诉你,我们为什么要做审计,以及如何做好一个审计模块!
当然,本篇不是架构篇,而是思想篇,我相信本篇文章,会为你带来更高的视角去俯瞰,去感悟。
技术角度:
一个优秀的审计模块,需要围绕着 "记录、存储、分析、防篡改" 等四个核心环节展开。
1、记录:
最好采用无侵入性设计,比如采用拦截器,
- 记录要素 : 谁(Who)、在何时(When)、对什么资源(What)、做了什么操作(How/Action)、结果如何(Success/Fail),以及操作环境(IP、设备、位置)。
这些仅是最简单的(5W1H),通常还需要操作前后改动的变化。以及风险评估。 - 数据来自哪里:通常是,敏感数据的访问、系统配置的变更、高危接口的调用、权限的分配等。
2、存储:
这个通常就要看情况而定了。
- 高频率型:比如记录全量日志,所有人的所有操作,这种情况,通常需要结合消息队列(MQ)进行削峰填谷 和做缓冲了。
- 低频率型:比如企业内部系统的业务级审计,就是只针对管理员、运营、风控、财务等关键角色的 "高风险改动",那么审计模块的设计就应该秉持**"精准抓要害、不做无效记录"**的原则。由于超低频,这时,通常直接放入数据即可。
3、分析:
这个需结合情况而定,看是否需要了。
因为你可以结合设计,设计具体的规则 。
举个简单的例子:(如:连续5次登录失败、凌晨进行批量数据导出、异地 IP 权限变更),触发实时告警(通过邮件、企微、钉钉)。
4、防篡改:
这个是相当重要的,毕竟审计就是为了排查隐患!
所以其中一条铁律,就是不可更改 。
复杂方法:通常采用哈希链技术。(计算第一个哈希块的哈希值,并且后续日志,计算哈希值时,都会依赖前一个)
简单方法:将表设计为只读、只可添加、(不可删除和修改),最好也能对审计日志做哈希计算,在核实的时候,可以同步校验数据是否被篡改。
人性角度:
刚接触,审计模块时,为了懒省事,我就常常幻想,
我只提供一个"接收日志"的接口,把所有的包袱都甩给业务方(让调用者自己去查旧值、查新值、比对差异再传给我),这样我就可以省很多事情。
但若真,这么设计,
这个审计模块在企业内部大概率会流产,或者沦为摆设。
原因很简单:人性是趋利的,业务开发人员最讨厌麻烦 。
如果改动一个财务金额,他们还要额外写几十上百行代码去查历史、对比字段、组装数据调用自己的接口,
他们要么会漏掉审计,要么会敷衍传入一堆垃圾数据。
所以,为了让模块真正落地,我们的审计模块应该提供"半自动化"的组件,而不是一个冷冰冰的 API 接口。
我这里拿go语言举例子,其他什么语言、框架也都适用,
去实现 微服务架构。
就可以这样设计。
go
+-------------------------------------------------------------------------+
| Common 公共基础库 (全公司共享的 Go Mod) |
| |
| +---------------------------+ +-------------------------------+ |
| | 1. audit.Record 记录包 | ----> | 2. send 异步发送包 (内置生产者) | |
| +---------------------------+ +-------------------------------+ |
+---------------+---------------------------------------------------------+
| (引入依赖)
v
+-------------------------------+ (异步投递: Kq/Kafka 或 RPC)
| 业务微服务 (如: 财务/风控/运营) | ----------------------------------+
+-------------------------------+ |
v
+--------------------------------+
| 3. 审计中心微服务 (独立) |
| |
| +------------------------+ |
| | audit-mq / audit-rpc | |
| +-----------+------------+ |
| | (写入) |
| v |
| [ 审计专用数据库 ] |
| | (查询) |
| v |
| +------------------------+ |
| | audit-api (展示端) | |
| +------------------------+ |
+---------------+----------------+
|
v
[ 运营后台/风控看板 ]
你的sdk,通常,会放到common公用包里面,当作一个SDK。
go
package audit
import (
"context"
"encoding/json"
"runtime/debug"
"time"
"://github.com"
)
// Options 审计配置项
type Options struct {
Module string // 模块名称(如:财务模块、风控模块)
Action string // 操作动作(如:修改放款金额、调整黑名单)
BizID string // 业务主键ID(如:订单号、用户ID)
FetchOld func(ctx context.Context) (interface{}, error) // 业务方提供的查旧数据函数
}
// Record 核心闭包包装器
func Record(ctx context.Context, opts Options, bizFunc func() error) error {
// 1. 从 go-zero 的 Context 中捞出当前操作人的信息(通过 JWT 传递过来的)
operator, _ := ctx.Value("username").(string)
if operator == "" {
operator = "system_unknown" // 兜底机制
}
// 2. 在修改前,安全地查询旧数据
var oldDataStr string = "{}"
if opts.FetchOld != nil {
// 给查询旧数据加上严格的超时限制(如 500毫秒),防止数据库卡死影响业务
oldCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
// 使用 defer recover 机制,防止业务方写的 FetchOld 函数自己发生 panic 导致整机崩溃
func() {
defer func() {
if r := recover(); r != nil {
logx.WithContext(ctx).Errorf("[Audit Panic] FetchOld panic: %v, stack: %s", r, string(debug.Stack()))
oldDataStr = `{"_audit_err": "fetch old data panic"}`
}
}()
if oldData, err := opts.FetchOld(oldCtx); err == nil && oldData != nil {
if bytes, mErr := json.Marshal(oldData); mErr == nil {
oldDataStr = string(bytes)
}
} else if err != nil {
logx.WithContext(ctx).Errorf("[Audit Error] 获取旧数据失败: %v", err)
oldDataStr = `{"_audit_err": "fetch old data database timeout or error"}`
}
}()
cancel()
}
// 3. 执行真正的业务逻辑(即业务方传进来的修改数据库操作)
err := bizFunc()
if err != nil {
// 关键点:如果业务本身报错了(如余额不足、数据库主键冲突),审计不记录,直接返回错误
return err
}
// 4. 业务执行成功后,再次安全地查询新数据
var newDataStr string = "{}"
if opts.FetchOld != nil {
newCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
func() {
defer func() {
if r := recover(); r != nil {
newDataStr = `{"_audit_err": "fetch new data panic"}`
}
}()
if newData, err := opts.FetchOld(newCtx); err == nil && newData != nil {
if bytes, mErr := json.Marshal(newData); mErr == nil {
newDataStr = string(bytes)
}
}
}()
cancel()
}
// 5. 异步发送给审计中心(绝对不能阻塞正常的业务响应)
go func() {
defer func() { recover() }() // 确保异步协程不会挂掉整个进程
// 组装最终的审计日志结构体
logData := map[string]interface{}{
"operator": operator,
"module": opts.Module,
"action": opts.Action,
"biz_id": opts.BizID,
"old_data": oldDataStr,
"new_data": newDataStr,
"change_time": time.Now().Format("2006-01-02 15:04:05"),
}
// 调用投递方法(可以直接写库,也可以通过 go-queue 发送给 Kafka)
sendToAuditCenter(logData)
}()
return nil
}
func sendToAuditCenter(data map[string]interface{}) {
// 这里实现你的上报逻辑
// 方式A:如果系统小,可以直接用 gorm/sqlx 往 audit_log 表里一条 insert
// 方式B:如果高并发,可以用 go-zero 自带的 MQ 组件发送到消息队列
}
开发者,的接入方式:
go
func (l *UpdateFinanceLogic) ChangeSalary(req *types.ChangeSalaryReq) error {
// 显式调用你的闭包包装器
err := audit.Record(l.ctx, audit.Options{
Module: "财务模块",
Action: "调整员工薪资",
BizID: string(req.EmployeeID),
// 业务方只需要告诉你,怎么拿这个员工的数据
FetchOld: func(ctx context.Context) (interface{}, error) {
return l.svcCtx.SalaryModel.FindOne(ctx, req.EmployeeID)
},
}, func() error {
// 这里是原本他们写的、真正的业务更新代码
return l.svcCtx.SalaryModel.Update(l.ctx, &model.Salary{
EmployeeID: req.EmployeeID,
Amount: req.NewAmount,
})
})
return err
}
安全角度
这个,其实就是写审计模块的初衷。
一、国家层面:
满足法律与行业监管(合规合规)很多行业(尤其是金融、医疗、政务、SaaS)没有审计模块属于违法或无法通过认证。
- 法律要求: 国家网络安全等级保护(等保)明确要求,系统必须保留至少 6个月 以上的审计日志。
- 上市公司 : 需要满足 SOX(萨班斯法案)等合规审计,证明公司的数据没有被内部高管随意操纵。
这些都是很正常的。
二、公司层面:
抓出内鬼和故障定位(事后追责)
当系统出现问题或数据被恶意篡改时,审计模块是唯一的线索来源。
- 删库跑路: 数据库被人恶意清空,审计模块能精准定位到是哪个员工账号、在哪个 IP 地址、用什么终端执行了该命令。
- 数据改错: 财务系统里的订单金额被修改了,审计可以查出是由于哪个操作员手误修改,还是系统接口异常导致的变动。
三、防范与未然
审计模块配合告警系统,可以变成主动防御的武器。
- 异地登录: 某个平时在上海办公的账号,突然凌晨3点在海外 IP 登录。
- 疯狂拖库 : 某个普通员工的账号,突然在 1 分钟内连续下载了 500 次客户敏感数据。
审计模块抓取到这些异常后,会立刻触发告警甚至直接冻结账号。
当然了,这个得看具体的需求了,不是超大型,超正规的,一般都不会需要这些。