Files
kim.dev.6789 b7f8db7d08 复制项目
2026-01-14 22:35:45 +08:00

293 lines
9.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package util
import (
"context"
"fmt"
"net"
"strings"
"git.imall.cloud/openim/chat/pkg/common/mctx"
"github.com/gin-gonic/gin"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
)
type Api struct {
ImUserID string
ProxyHeader string
ChatAdminUserID string
}
func (o *Api) WithAdminUser(ctx context.Context) context.Context {
return mctx.WithAdminUser(ctx, o.ChatAdminUserID)
}
// isPrivateIP 检查IP是否是内网IP
func isPrivateIP(ip net.IP) bool {
if ip == nil {
return false
}
// 检查是否是内网IP范围
// 10.0.0.0/8
// 172.16.0.0/12
// 192.168.0.0/16
// 127.0.0.0/8 (localhost)
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true
}
ip4 := ip.To4()
if ip4 == nil {
return false
}
// 10.0.0.0/8
if ip4[0] == 10 {
return true
}
// 172.16.0.0/12
if ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31 {
return true
}
// 192.168.0.0/16
if ip4[0] == 192 && ip4[1] == 168 {
return true
}
return false
}
// parseForwardedHeader 解析标准的 Forwarded 头RFC 7239
// 格式Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
func parseForwardedHeader(forwarded string) string {
// 查找 for= 后面的IP地址
parts := strings.Split(forwarded, ";")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(strings.ToLower(part), "for=") {
ip := strings.TrimPrefix(part, "for=")
ip = strings.TrimPrefix(ip, "For=")
ip = strings.TrimPrefix(ip, "FOR=")
// 移除可能的引号和端口号
ip = strings.Trim(ip, `"`)
if idx := strings.Index(ip, ":"); idx != -1 {
ip = ip[:idx]
}
if parsedIP := net.ParseIP(ip); parsedIP != nil {
return ip
}
}
}
return ""
}
// GetClientIP 获取客户端真实IP地址
// 支持多层反向代理和CDN场景
// 优先级:
// 1. CDN特定头CF-Connecting-IP, True-Client-IP等- 最可靠
// 2. 标准 Forwarded 头RFC 7239- 现代标准
// 3. X-Real-IP - 通常由第一层代理设置比X-Forwarded-For更可靠
// 4. X-Forwarded-For - 取最左边的IP客户端真实IP格式client_ip, proxy1_ip, proxy2_ip, ...
// 5. 其他常见头X-Client-IP, True-Client-IP等
// 6. 自定义ProxyHeader
// 7. RemoteAddr - 最后回退
func (o *Api) GetClientIP(c *gin.Context) (string, error) {
// 记录所有相关的HTTP头用于调试
headers := map[string]string{
"Forwarded": c.Request.Header.Get("Forwarded"), // RFC 7239 标准头
"X-Forwarded-For": c.Request.Header.Get("X-Forwarded-For"),
"X-Real-IP": c.Request.Header.Get("X-Real-IP"),
"CF-Connecting-IP": c.Request.Header.Get("CF-Connecting-IP"), // Cloudflare - 真实客户端IP
"CF-Ray": c.Request.Header.Get("CF-Ray"), // Cloudflare - 请求ID用于验证来自CF
"True-Client-IP": c.Request.Header.Get("True-Client-IP"), // Cloudflare Enterprise, Akamai
"X-Client-IP": c.Request.Header.Get("X-Client-IP"), // 一些代理
"X-Forwarded": c.Request.Header.Get("X-Forwarded"), // 一些代理
"Forwarded-For": c.Request.Header.Get("Forwarded-For"), // 非标准,但有些代理使用
"X-Cluster-Client-IP": c.Request.Header.Get("X-Cluster-Client-IP"), // Kubernetes
"RemoteAddr": c.Request.RemoteAddr,
}
if o.ProxyHeader != "" {
headers["Custom-"+o.ProxyHeader] = c.Request.Header.Get(o.ProxyHeader)
}
// 打印所有能获取到的IP相关信息用于调试
customProxyHeader := ""
if o.ProxyHeader != "" {
customProxyHeader = headers["Custom-"+o.ProxyHeader]
}
log.ZInfo(c, "GetClientIP 调试信息 - 所有HTTP头",
"Forwarded", headers["Forwarded"],
"X-Forwarded-For", headers["X-Forwarded-For"],
"X-Real-IP", headers["X-Real-IP"],
"CF-Connecting-IP", headers["CF-Connecting-IP"],
"CF-Ray", headers["CF-Ray"],
"True-Client-IP", headers["True-Client-IP"],
"X-Client-IP", headers["X-Client-IP"],
"X-Forwarded", headers["X-Forwarded"],
"Forwarded-For", headers["Forwarded-For"],
"X-Cluster-Client-IP", headers["X-Cluster-Client-IP"],
"RemoteAddr", headers["RemoteAddr"],
"Custom-ProxyHeader", customProxyHeader)
// 1. 优先检查 Cloudflare 的 CF-Connecting-IP最可靠Cloudflare 直接设置真实客户端IP
// 检查是否来自 Cloudflare通过 CF-Ray 头判断)
cfRay := c.Request.Header.Get("CF-Ray")
cfConnectingIP := c.Request.Header.Get("CF-Connecting-IP")
if cfConnectingIP != "" {
// 特别记录 Cloudflare 相关信息
log.ZInfo(c, "GetClientIP detected Cloudflare request",
"CF-Connecting-IP", cfConnectingIP,
"CF-Ray", cfRay)
ip := strings.TrimSpace(cfConnectingIP)
parsedIP := net.ParseIP(ip)
if parsedIP != nil {
// Cloudflare 的 CF-Connecting-IP 总是包含真实客户端IP无论公网还是内网
// 如果同时有 CF-Ray说明确实来自 Cloudflare更加可靠
if cfRay != "" {
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", "CF-Connecting-IP",
"cfRay", cfRay,
"isPrivateIP", isPrivateIP(parsedIP))
} else {
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", "CF-Connecting-IP",
"isPrivateIP", isPrivateIP(parsedIP))
}
return ip, nil
}
}
// 2. 检查其他CDN特定的头
cdnHeaders := []string{
"True-Client-IP", // Cloudflare Enterprise, Akamai
"X-Client-IP", // 一些CDN和代理
"X-Cluster-Client-IP", // Kubernetes Ingress
}
for _, headerName := range cdnHeaders {
ipStr := c.Request.Header.Get(headerName)
if ipStr != "" {
ip := strings.TrimSpace(ipStr)
parsedIP := net.ParseIP(ip)
if parsedIP != nil {
// CDN头通常包含真实客户端IP优先返回公网IP
if !isPrivateIP(parsedIP) {
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", headerName,
"isPrivateIP", false)
return ip, nil
}
// 即使是内网IPCDN头也相对可靠
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", headerName,
"isPrivateIP", true)
return ip, nil
}
}
}
// 3. 检查标准 Forwarded 头RFC 7239- 现代标准,最可靠
forwarded := c.Request.Header.Get("Forwarded")
if forwarded != "" {
ip := parseForwardedHeader(forwarded)
if ip != "" {
parsedIP := net.ParseIP(ip)
if parsedIP != nil {
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", "Forwarded",
"isPrivateIP", isPrivateIP(parsedIP))
return ip, nil
}
}
}
// 4. 检查 X-Real-IP 头通常由第一层代理设置比X-Forwarded-For更可靠
xRealIP := c.Request.Header.Get("X-Real-IP")
if xRealIP != "" {
ip := strings.TrimSpace(xRealIP)
parsedIP := net.ParseIP(ip)
if parsedIP != nil {
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", "X-Real-IP",
"isPrivateIP", isPrivateIP(parsedIP))
return ip, nil
}
}
// 5. 检查 X-Forwarded-For 头格式client_ip, proxy1_ip, proxy2_ip, ...
// 重要在多层代理中最左边的IP是客户端真实IP应该优先取第一个IP
xForwardedFor := c.Request.Header.Get("X-Forwarded-For")
if xForwardedFor != "" {
ips := strings.Split(xForwardedFor, ",")
// 取最左边的IP第一个IP这是客户端真实IP
// 格式client_ip, proxy1_ip, proxy2_ip, ...
if len(ips) > 0 {
ip := strings.TrimSpace(ips[0])
parsedIP := net.ParseIP(ip)
if parsedIP != nil {
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", "X-Forwarded-For",
"totalIPs", len(ips),
"isPrivateIP", isPrivateIP(parsedIP),
"note", "取最左边的IP客户端真实IP")
return ip, nil
}
}
}
// 6. 检查其他常见头
otherHeaders := []string{"X-Client-IP", "X-Forwarded", "Forwarded-For"}
for _, headerName := range otherHeaders {
ipStr := c.Request.Header.Get(headerName)
if ipStr != "" {
ip := strings.TrimSpace(ipStr)
parsedIP := net.ParseIP(ip)
if parsedIP != nil {
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", headerName,
"isPrivateIP", isPrivateIP(parsedIP))
return ip, nil
}
}
}
// 7. 如果配置了自定义的 ProxyHeader检查它
if o.ProxyHeader != "" {
customHeaderIP := c.Request.Header.Get(o.ProxyHeader)
if customHeaderIP != "" {
ip := strings.TrimSpace(customHeaderIP)
parsedIP := net.ParseIP(ip)
if parsedIP != nil {
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", "Custom-"+o.ProxyHeader,
"isPrivateIP", isPrivateIP(parsedIP))
return ip, nil
}
}
}
// 8. 最后使用 RemoteAddr在集群环境中可能是代理服务器的IP
remoteAddr := c.Request.RemoteAddr
ip, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
log.ZError(c, "GetClientIP failed to parse RemoteAddr", err, "RemoteAddr", remoteAddr)
return "", errs.ErrInternalServer.WrapMsg(fmt.Sprintf("failed to parse RemoteAddr: %v", err))
}
parsedIP := net.ParseIP(ip)
log.ZInfo(c, "GetClientIP 最终选择IP",
"ip", ip,
"source", "RemoteAddr",
"isPrivateIP", parsedIP != nil && isPrivateIP(parsedIP))
return ip, nil
}
func (o *Api) GetDefaultIMAdminUserID() string {
return o.ImUserID
}