复制项目

This commit is contained in:
kim.dev.6789
2026-01-14 22:35:45 +08:00
parent 305d526110
commit b7f8db7d08
297 changed files with 81784 additions and 0 deletions

292
internal/api/util/api.go Normal file
View File

@@ -0,0 +1,292 @@
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
}