feat(log): 多源 clientIP 降级与去重 ip_chain

按 EO→XFF非回环→X-Real→RemoteAddr 选取 ip;访问日志附带去重 ip_chain;登录限流改用 clientIP。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-23 06:04:43 +08:00
parent 831a7f017a
commit 25faa93724
5 changed files with 189 additions and 19 deletions
+87 -13
View File
@@ -108,20 +108,24 @@ func (that *Application) initiateGracefulShutdown(drain, shutdown time.Duration)
})
}
// clientIP 取客户端真实 IPX-Real-IP → X-Forwarded-For 第一段 → RemoteAddr
func clientIP(req *http.Request) string {
if req == nil {
// isLoopbackIP 判断是否为回环地址(空串视为不可用)
func isLoopbackIP(ip string) bool {
ip = strings.TrimSpace(ip)
if ip == "" {
return true
}
parsed := net.ParseIP(ip)
if parsed == nil {
return false
}
return parsed.IsLoopback()
}
// remoteHost 从 RemoteAddr 取出 host(去掉端口)
func remoteHost(req *http.Request) string {
if req == nil || req.RemoteAddr == "" {
return ""
}
if ip := strings.TrimSpace(req.Header.Get("X-Real-IP")); ip != "" {
return ip
}
if xff := req.Header.Get("X-Forwarded-For"); xff != "" {
if i := strings.Index(xff, ","); i >= 0 {
return strings.TrimSpace(xff[:i])
}
return strings.TrimSpace(xff)
}
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err == nil && host != "" {
return host
@@ -129,6 +133,72 @@ func clientIP(req *http.Request) string {
return req.RemoteAddr
}
// resolveClientIP 多源识别客户端 IP。
// bestEO-Connecting-IP → XFF 第一个非回环 → 非回环 X-Real-IP → RemoteAddr
// XFF 先于 X-Real:反代误把 CDN 节点写进 X-Real-IP 时仍能落到客户端)
// chain:各源头 IP 按首次出现顺序逗号拼接,同一 IP 只保留一次
func resolveClientIP(req *http.Request) (best string, chain string) {
if req == nil {
return "", ""
}
eo := strings.TrimSpace(req.Header.Get("EO-Connecting-IP"))
xri := strings.TrimSpace(req.Header.Get("X-Real-IP"))
xffRaw := req.Header.Get("X-Forwarded-For")
ra := remoteHost(req)
var xffParts []string
if xffRaw != "" {
for _, p := range strings.Split(xffRaw, ",") {
if p = strings.TrimSpace(p); p != "" {
xffParts = append(xffParts, p)
}
}
}
seen := make(map[string]struct{}, 4)
var ordered []string
add := func(ip string) {
ip = strings.TrimSpace(ip)
if ip == "" {
return
}
if _, ok := seen[ip]; ok {
return
}
seen[ip] = struct{}{}
ordered = append(ordered, ip)
}
add(eo)
add(xri)
for _, p := range xffParts {
add(p)
}
add(ra)
chain = strings.Join(ordered, ",")
if eo != "" && !isLoopbackIP(eo) {
return eo, chain
}
for _, p := range xffParts {
if !isLoopbackIP(p) {
return p, chain
}
}
if xri != "" && !isLoopbackIP(xri) {
return xri, chain
}
if ra != "" {
return ra, chain
}
return "", chain
}
// clientIP 取客户端真实 IP(见 resolveClientIP
func clientIP(req *http.Request) string {
ip, _ := resolveClientIP(req)
return ip
}
// shortID 生成 n 字节随机 hexn=6 → 12 位)
func shortID(n int) string {
b := make([]byte, n)
@@ -504,13 +574,17 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
defer func() {
//是否展示日志
if that.WebConnectLog != nil {
ip, ipChain := resolveClientIP(req)
evt := that.WebConnectLog.Info().
Str("ip", clientIP(req)).
Str("ip", ip).
Str("method", context.Req.Method).
Str("sid", sid).
Str("request_id", requestId).
Float64("cost_ms", ObjToFloat64(time.Now().UnixNano()-nowUnixTime.UnixNano())/1000000.00).
Float64("size_kb", ObjToFloat64(context.DataSize)/1000.00)
if ipChain != "" && ipChain != ip {
evt = evt.Str("ip_chain", ipChain)
}
if country := req.Header.Get("EO-Client-IPCountry"); country != "" {
evt = evt.Str("ip_country", country)
}