| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- package audit
- import (
- "bytes"
- "database/sql"
- "encoding/json"
- "fmt"
- "io"
- "net"
- "net/http"
- "strings"
- "time"
- "udeServer/internal/log"
- udeModel "udeServer/internal/model"
- "udeServer/internal/response"
- )
- // 捕获响应
- type wrappedResponseWriter struct {
- http.ResponseWriter
- statusCode int
- body *bytes.Buffer
- }
- func newWrappedResponseWriter(w http.ResponseWriter) *wrappedResponseWriter {
- return &wrappedResponseWriter{
- ResponseWriter: w,
- statusCode: http.StatusOK,
- body: bytes.NewBuffer(nil),
- }
- }
- // Write re-records the response body (buffered for the audit entry).
- func (w *wrappedResponseWriter) Write(b []byte) (int, error) {
- w.body.Write(b)
- return w.ResponseWriter.Write(b)
- }
- // WriteHeader re-records the status code.
- func (w *wrappedResponseWriter) WriteHeader(code int) {
- w.statusCode = code
- w.ResponseWriter.WriteHeader(code)
- }
- // genericResp parses the unified response envelope.
- type genericResp struct {
- Code int `json:"code"`
- Message string `json:"message"`
- Data json.RawMessage `json:"data"`
- }
- // skipLogPaths silences self-generation noise: the log management endpoints
- // would otherwise audit every entry they produce, and the archive download
- // response is a huge binary blob with no audit value.
- var skipLogPaths = map[string]bool{
- "/api/v1/systemLog/list": true,
- "/api/v1/systemLog/audit": true,
- "/api/v1/systemLog/link": true,
- "/api/v1/logRestore/list": true,
- "/api/v1/logArchive/list": true,
- "/api/v1/logArchive/download": true,
- }
- // LogMiddleware audits every HTTP request: it captures request/response
- // bodies, computes digests, uploads the raw bodies to MinIO, builds a
- // t_system_log record guarded by an HMAC-SM3 signature and stores it
- // asynchronously. Deleted per sdf-serve build: the entry is built from the
- // unified response envelope (udeServer answers HTTP 200 always, business
- // codes live in the body), so ResponseCode records the body code.
- type LogMiddleware struct {
- }
- func NewLogMiddleware() *LogMiddleware {
- return &LogMiddleware{}
- }
- // Handle is the middleware closure, constructed with the system-log and
- // config models plus the service name of this deployment.
- func (m *LogMiddleware) Handle(logModel udeModel.TSystemLogModel, configModel udeModel.TConfigModel, serviceName string) func(next http.HandlerFunc) http.HandlerFunc {
- return func(next http.HandlerFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- if skipLogPaths[r.URL.Path] {
- next.ServeHTTP(w, r)
- return
- }
- // 1.获取TraceId和SpanId以及请求元数据
- ctx, traceID, spanID, userID, userName, _ := EnsureTraceAndSpanID(r.Context())
- // 2.读请求体
- reqBody, _ := io.ReadAll(r.Body)
- r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
- reqHeader := filterRequestHeader(r.Header)
- log.RecordDebug(ctx, "请求头:%v", reqHeader)
- // 3.捕获响应
- rw := newWrappedResponseWriter(w)
- start := time.Now()
- next.ServeHTTP(rw, r.WithContext(ctx))
- duration := time.Since(start).Milliseconds()
- // 4.解析响应统一封装
- respBody := rw.body.Bytes()
- var resp genericResp
- httpCode := int64(response.CodeServerError)
- message := ""
- parseFailed := false
- if err := json.Unmarshal(respBody, &resp); err != nil {
- parseFailed = true
- log.RecordWarn(ctx, "[LogHandle] 无法解析响应 JSON, Path:%s, Status:%d, Raw:%s",
- r.URL.Path, rw.statusCode, string(respBody))
- } else {
- httpCode = int64(resp.Code)
- message = resp.Message
- if resp.Code != response.CodeOK {
- log.RecordError(ctx, fmt.Errorf("[Request-Fail] Path=%s Code=%d Msg=%q",
- r.URL.Path, resp.Code, resp.Message))
- } else {
- log.RecordInfo(ctx, "[Request-Success] Path=%s Code=%d Msg=%s Data:%+v",
- r.URL.Path, resp.Code, resp.Message, string(resp.Data))
- }
- }
- // 5. 上传 & 摘要
- client, logConfig := GetGlobalConfig(configModel)
- reqDigestStr, reqUrl, reqErr := UploadAndDigest(reqBody, traceID, spanID, "[Http-Request]", client, r.Header.Get("Content-Type"))
- if reqErr != nil {
- log.RecordError(ctx, reqErr)
- }
- respDigestStr, respUrl, respErr := UploadAndDigest(respBody, traceID, spanID, "[Http-Response]", client, "application/json")
- if respErr != nil {
- log.RecordError(ctx, respErr)
- }
- // 6.构造日志对象
- entry := &udeModel.TSystemLog{
- Iden: NewUUID(),
- TraceId: traceID,
- SpanId: spanID,
- ParentSpanId: sql.NullString{String: "", Valid: false}, //顶层链路父SpanId为空时填写null
- Type: SystemLog,
- ServiceName: serviceName,
- UriPath: sql.NullString{String: r.URL.Path, Valid: true},
- HttpMethod: sql.NullString{String: r.Method, Valid: true},
- Method: deriveHandlerName(r.URL.Path),
- Level: ChooseLevelByBusinessCode(httpCode),
- AppKey: sql.NullString{Valid: false},
- UserId: sql.NullString{String: userID, Valid: userID != ""},
- UserName: sql.NullString{String: userName, Valid: userName != ""},
- ClientIp: sql.NullString{String: GetClientIPPreferPublic(r), Valid: true},
- RequestHeader: reqHeader,
- RequestBodyDigest: reqDigestStr,
- RequestBodyFileKey: reqUrl,
- ResponseCode: httpCode,
- ResponseBodyDigest: respDigestStr,
- ResponseBodyFileKey: respUrl,
- DurationMs: duration,
- CreatedAt: time.Now(),
- AuditStatus: UnAudited,
- }
- // 7.填充ErrMsg
- switch {
- case parseFailed:
- entry.ErrorMessage = sql.NullString{String: "Invalid response format", Valid: true}
- case resp.Code != response.CodeOK:
- entry.ErrorMessage = sql.NullString{String: message, Valid: true}
- default:
- entry.ErrorMessage = sql.NullString{Valid: false}
- }
- // 8.填充Hmac值,复用缓冲区;密钥为空时用空串计算,保持列非空
- macBuffer := make([]byte, 0, 32)
- if mac, hErr := CalculateHMAC(entry, logConfig.HmacSecret, macBuffer); hErr == nil {
- entry.HMac = mac
- } else {
- log.RecordError(ctx, hErr)
- }
- // 9.按配置库中 log_record_level 过滤后异步入库
- save := SaveToDBByLogLevel(logConfig.LogRecordLevel, entry.Level)
- if save {
- go AsyncInsertLog(logModel, entry)
- }
- }
- }
- }
- // deriveHandlerName derives the business handler name from the request path
- // (last path segment + "Handler").
- func deriveHandlerName(path string) string {
- clean := strings.Trim(path, "/")
- parts := strings.Split(clean, "/")
- if len(parts) == 0 {
- return ""
- }
- last := parts[len(parts)-1]
- return last + "Handler"
- }
- var keepHeaders = map[string]bool{
- "content-type": true,
- // x-api-key/x-user-id 等 udeServer 还没有鉴权体系,暂无注入来源
- }
- // filterRequestHeader keeps only whitelisted request headers.
- func filterRequestHeader(header map[string][]string) sql.NullString {
- filtered := make(map[string][]string)
- for k, v := range header {
- if keepHeaders[strings.ToLower(k)] {
- filtered[k] = v
- }
- }
- if headerBytes, err := json.Marshal(filtered); err == nil {
- return sql.NullString{
- String: string(headerBytes),
- Valid: true,
- }
- }
- return sql.NullString{Valid: false}
- }
- // GetClientIPPreferPublic extracts the client IP, preferring public IPs.
- func GetClientIPPreferPublic(r *http.Request) string {
- ip := getClientIP(r, true)
- if ip == "" {
- ip = getClientIP(r, false)
- }
- return ip
- }
- // getClientIP resolves the client IP honouring proxy headers, optionally
- // hiding private addresses.
- func getClientIP(r *http.Request, excludePrivate bool) string {
- // 1. 从 X-Forwarded-For 获取第一个真实IP
- xff := r.Header.Get("X-Forwarded-For")
- if xff != "" {
- parts := strings.Split(xff, ",")
- for _, ip := range parts {
- ip = strings.TrimSpace(ip)
- if ip != "" && (!excludePrivate || isPublicIP(ip)) {
- return ip
- }
- }
- }
- // 2. 从 X-Real-IP 获取反向代理地址
- if ip := strings.TrimSpace(r.Header.Get("X-Real-IP")); ip != "" {
- if !excludePrivate || isPublicIP(ip) {
- return ip
- }
- }
- // 3. 从 RemoteAddr 获取代理地址
- ip, _, err := net.SplitHostPort(r.RemoteAddr)
- if err == nil && (!excludePrivate || isPublicIP(ip)) {
- return ip
- }
- return ""
- }
- // isPublicIP reports whether the IP address is public/external.
- func isPublicIP(ip string) bool {
- parsedIP := net.ParseIP(ip)
- if parsedIP == nil {
- return false
- }
- privateBlocks := []*net.IPNet{
- // IPv4 私有地址段
- {IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
- {IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
- {IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
- // IPv6 loopback
- {IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
- }
- for _, block := range privateBlocks {
- if block.Contains(parsedIP) {
- return false
- }
- }
- return true
- }
|