From 1e36f21ec4e79329e1bb213c16428d5b9f5f902d Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 29 Apr 2026 11:53:06 +0800 Subject: [PATCH] fix(user): withoutcancel audit, x-forwarded-for client ip --- internal/user/handler.go | 21 ++++++++++++++++++++- internal/user/service.go | 10 ++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/internal/user/handler.go b/internal/user/handler.go index b7c6248..b7f3b0f 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -97,7 +97,7 @@ func (h *Handler) login(w http.ResponseWriter, r *http.Request) { return } - tok, u, err := h.svc.Login(r.Context(), req.Email, req.Password, r.RemoteAddr) + tok, u, err := h.svc.Login(r.Context(), req.Email, req.Password, clientIP(r)) if err != nil { httpx.WriteError(w, rid, err) return @@ -142,3 +142,22 @@ func bearerFrom(r *http.Request) string { } return strings.TrimSpace(h[len(bearerPrefix):]) } + +// clientIP 从请求头提取最接近真实客户端的 IP。优先级:X-Forwarded-For 首段 +// > X-Real-IP > r.RemoteAddr。 +// +// **安全提示**:当前未实现 trusted proxy 名单,X-Forwarded-For/X-Real-IP 可 +// 被任意客户端伪造。生产部署必须在反向代理层 strip 客户端送来的这些头,仅 +// 由代理本身写入;否则审计 IP 不可信。该问题在引入 ingress 配置时一并处理。 +func clientIP(r *http.Request) string { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + if comma := strings.IndexByte(xff, ','); comma > 0 { + return strings.TrimSpace(xff[:comma]) + } + return strings.TrimSpace(xff) + } + if xri := r.Header.Get("X-Real-IP"); xri != "" { + return strings.TrimSpace(xri) + } + return r.RemoteAddr +} diff --git a/internal/user/service.go b/internal/user/service.go index 30bacc0..8172561 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -77,9 +77,12 @@ func (s *service) Login(ctx context.Context, email, password, ip string) (string return "", nil, err } // 审计是 best-effort:_ = 吞掉错误,审计落库失败不能导致登录失败。 + // 用 context.WithoutCancel 派生:HTTP handler 在响应返回后 r.Context() 会 + // 被取消,但审计写入必须完成(漏行比写错更危险)。timeout 由调用方在 + // ctx 上不再控制,audit recorder 自身需保证不无限阻塞。 if s.audit != nil { uid := u.ID - _ = s.audit.Record(ctx, audit.Entry{ + _ = s.audit.Record(context.WithoutCancel(ctx), audit.Entry{ UserID: &uid, Action: "user.login", TargetType: "user", @@ -102,9 +105,12 @@ func (s *service) Logout(ctx context.Context, token string) error { return err } // 审计是 best-effort:_ = 吞掉错误;uid == uuid.Nil 表示 token 无效,跳过。 + // 用 context.WithoutCancel 派生:HTTP handler 在响应返回后 r.Context() 会 + // 被取消,但审计写入必须完成(漏行比写错更危险)。timeout 由调用方在 + // ctx 上不再控制,audit recorder 自身需保证不无限阻塞。 if s.audit != nil && uid != uuid.Nil { u := uid - _ = s.audit.Record(ctx, audit.Entry{ + _ = s.audit.Record(context.WithoutCancel(ctx), audit.Entry{ UserID: &u, Action: "user.logout", TargetType: "user",