100行代碼實(shí)現(xiàn)審計(jì)日志中間件
本文轉(zhuǎn)載自微信公眾號(hào)「小小平頭哥」,作者小小平頭哥。轉(zhuǎn)載本文請(qǐng)聯(lián)系小小平頭哥公眾號(hào)。
審計(jì)日志管理是我們?cè)趙eb系統(tǒng)開發(fā)中的常見的模塊,雖然它有時(shí)并不屬于業(yè)務(wù)模塊的范疇,但對(duì)于系統(tǒng)整體來(lái)說(shuō)卻十分關(guān)鍵,用戶的操作(尤其是關(guān)鍵操作)、用戶的登錄,我們的系統(tǒng)都應(yīng)加以記錄,以便后續(xù)溯源。
日志管理的方案可以看到很多,本文介紹的是一種基于Golang Gin框架的自定義中間件的實(shí)現(xiàn)方案,為大家拋磚引玉了。
個(gè)人認(rèn)為有以下幾個(gè)優(yōu)勢(shì):
(1)中間件的方式可靈活地匹配路由組,從而靈活地指定需要記錄日志的路由組;
(2)同一個(gè)路由組中通過(guò)context value 來(lái)區(qū)分接口是否需要記錄操作日志;
(3)業(yè)務(wù)處理函數(shù)中可靈活配置需記錄內(nèi)容,不需集中處理。
本文轉(zhuǎn)載自微信公眾號(hào)「小小平頭哥」,作者小小平頭哥。轉(zhuǎn)載本文請(qǐng)聯(lián)系小小平頭哥公眾號(hào)。
01整體流程
1) 中間件函數(shù)整體的流程
圖片
2) 業(yè)務(wù)函數(shù)流程
圖片
02代碼實(shí)現(xiàn)
1) 中間件函數(shù)實(shí)現(xiàn)
type Response struct {
Code int `json:"code" bson:"code"`
}
type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
const (
HttpRespSuccessCode = 0
)
// Logger 日志記錄
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
//備份請(qǐng)求體
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = blw
//繼續(xù)執(zhí)行請(qǐng)求
c.Next()
//判斷記錄標(biāo)志
needToLog, ok := c.Get("need_to_log")
if !ok {
log.Warn("獲取是否需要記錄日志失敗")
return
}
if !needToLog.(bool) {
return
}
//也可以在這兒加入白名單 判斷是否是不需記錄的URL
/*
url := c.Request.RequestURI
if strings.Index(url, "logout") > -1 ||
strings.Index(url, "login") > -1 {
return
}
*/
// 獲取請(qǐng)求的HTTP狀態(tài)碼
statusCode := c.Writer.Status()
// 獲取請(qǐng)求IP
clientIP := common.GetClientIP(c)
isSuccess := false
//若HTTP狀態(tài)碼為200
if c.Writer.Status() == http.StatusOK {
var resp Response
// 獲取返回的數(shù)據(jù)
err := json.Unmarshal(blw.body.Bytes(), &resp)
if err != nil {
log.Warn("Logs Operation Unmarshal Error: %s", err.Error())
return
}
//判斷操作是否成功 需結(jié)合業(yè)務(wù)函數(shù)的返回值結(jié)構(gòu)
if resp.Code == HttpRespSuccessCode {
isSuccess = true
}
}
if statusCode != http.StatusNotFound {
SetDBLog(c, clientIP, isSuccess)
}
}
}
// SetDBLog 寫入日志表
func SetDBLog(c *gin.Context, clientIP string, status bool) {
user, ok := c.Get("user")
if !ok {
log.Warn("審計(jì)日志-獲取用戶名失敗")
}
//日志格式化 然后入庫(kù)
logInfo := table.Logs{}
//構(gòu)造日志ID 可使用其他方式替代
logInfo.LogID = NewNanoid()
if user != nil {
logInfo.Username = user.(string)
}
operatorType, exist := c.Get("operation_type")
if exist {
logInfo.OperationType = operatorType.(string)
}
logInfo.IP = clientIP
operation, exist := c.Get("operation")
if exist {
logInfo.Description = operation.(string)
}
if status == true {
logInfo.Description = logInfo.Description + "成功"
} else {
logInfo.Description = logInfo.Description + "失敗"
}
//日志入庫(kù)
err := InsertLog(logInfo)
if err != nil {
log.Warn("InsertLog %s error, %s", logInfo.LogID, err.Error())
}
}
// InsertLog 插入log
func InsertLog(logs table.Logs) error {
}
2) 業(yè)務(wù)函數(shù)實(shí)現(xiàn)
func (User) UserLoginOut(c *ctx.Context) {
//設(shè)定記錄日志標(biāo)志
c.Set("need_to_log", true)
//設(shè)定操作類型
c.Set("operation_type", "用戶退出登錄")
//設(shè)定具體操作
c.Set("operation", "用戶退出登錄")
c.Success()
}
3) 路由組應(yīng)用
//設(shè)定路由組
UserRouter := apiV1Group.Group("users")
//為路由組應(yīng)用中間件
UserRouter.Use(middleware.Logger())
03注意事項(xiàng)
1) 中間件處理函數(shù)中的備份原始請(qǐng)求體很重要,否則可能會(huì)出現(xiàn)業(yè)務(wù)代碼無(wú)法獲取請(qǐng)求參數(shù)的情況;
- 中間件的報(bào)錯(cuò)不應(yīng)影響原有業(yè)務(wù)邏輯。