应优先读X-Real-IP,若为空且RemoteAddr属可信代理,则取X-Forwarded-For中最右非私有IP,否则用RemoteAddr剥离端口;需显式配置trustedProxies并校验IP合法性。

如何在 Go HTTP Handler 中提取真实客户端 IP
微服务里直接用 r.RemoteAddr 拿到的通常是反向代理(如 Nginx、API 网关)的内网地址,不是用户真实 IP。必须从请求头中解析,但不能无条件信任 X-Forwarded-For —— 它可被伪造。
正确做法是结合 X-Real-IP(Nginx 默认设置)或可信的 X-Forwarded-For 链,并限制只从已知代理 IP 列表后取值。例如:
func getClientIP(r *http.Request, trustedProxies []string) string {
ip := r.Header.Get("X-Real-IP")
if ip != "" {
return ip
}
// 仅当 RemoteAddr 在 trustedProxies 中时,才信任 X-Forwarded-For 最右非私有 IP
if len(trustedProxies) > 0 && isTrustedProxy(r.RemoteAddr, trustedProxies) {
xff := r.Header.Get("X-Forwarded-For")
if xff != "" {
for _, ip := range strings.Split(xff, ",") {
ip = strings.TrimSpace(ip)
if !net.IsPrivateIP(ip) {
return ip
}
}
}
}
return r.RemoteAddr
}
-
trustedProxies必须显式配置,比如[]string{"10.0.0.1", "10.0.0.2"},不能留空 -
net.IsPrivateIP需自己实现(标准库无此函数),判断是否为10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.1等 - 若服务直连公网(无前置代理),直接用
r.RemoteAddr并剥离端口即可:strings.Split(r.RemoteAddr, ":")[0]
如何加载和热更新黑白名单规则
硬编码或每次请求都读文件会拖慢性能,也难动态生效。推荐用内存 map + 原子变量 + 定期轮询或信号触发 reload。
示例结构:
type IPRule struct {
Whitelist map[string]struct{} // key 是 CIDR 或单 IP
Blacklist map[string]struct{}
mu sync.RWMutex
}
var rules atomic.Value // 存储 *IPRule
func loadRulesFromFile(path string) (*IPRule, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg struct {
Whitelist []string `json:"whitelist"`
Blacklist []string `json:"blacklist"`
}
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
wl := make(map[string]struct{})
for _, cidr := range cfg.Whitelist {
wl[cidr] = struct{}{}
}
bl := make(map[string]struct{})
for _, cidr := range cfg.Blacklist {
bl[cidr] = struct{}{}
}
return &IPRule{Whitelist: wl, Blacklist: bl}, nil
}
- 初始化时调用
rules.Store(loadRulesFromFile(...)) - 检查逻辑中用
rules.Load().(*IPRule)读取,避免锁竞争 - 热更新时先
newRules := loadRulesFromFile(...),再rules.Store(newRules),原子替换 - CIDR 支持很重要:用
net.IPNet解析后调用*net.IPNet.Contains(ip)判断归属
如何在 Gin / Echo / 标准 net/http 中统一拦截
无论框架怎么选,核心逻辑都在中间件里做 IP 检查,失败直接返回 403 Forbidden,不进业务 handler。
Gin 示例:
func IPFilter(trustedProxies []string) gin.HandlerFunc {
return func(c *gin.Context) {
ip := getClientIP(c.Request, trustedProxies)
rule := rules.Load().(*IPRule)
rule.mu.RLock()
defer rule.mu.RUnlock()
allowed := false
if len(rule.Whitelist) > 0 {
allowed = inCIDRList(ip, rule.Whitelist)
} else {
allowed = !inCIDRList(ip, rule.Blacklist)
}
if !allowed {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
}
- 白名单优先级高于黑名单:只要配置了白名单,就只放行列表内 IP,黑名单自动失效
-
inCIDRList需遍历所有 CIDR 字符串,用net.ParseCIDR解析后比对,注意错误忽略(非法 CIDR 跳过) - 不要在中间件里做耗时操作(如查 DB),否则拖慢所有请求
- Echo 和标准
http.Handler写法类似,只是接口签名不同,核心逻辑完全复用
为什么不能只靠中间件做黑白名单
中间件能拦住大部分流量,但无法覆盖所有路径:gRPC 接口、健康检查端点、Prometheus metrics 暴露、pprof 调试接口等常绕过 HTTP 中间件链。
- gRPC 没有传统中间件概念,需在
UnaryInterceptor或StreamInterceptor里手动解析 peer IP:peer.FromContext(c.Context()) - 健康检查(如
/healthz)建议单独放行,或用独立 IP 规则,避免误杀运维探测 - 若服务同时暴露 HTTP + gRPC + WebSocket,每种协议都要各自实现 IP 检查,逻辑不能复用中间件
- 更隐蔽的风险:Kubernetes Pod IP 或 Service ClusterIP 可能被当作客户端 IP 传入,尤其在 HostNetwork 模式下,需提前过滤
真正上线前,得挨个验证每个监听端口、每种协议入口、每个特殊路径的行为,IP 黑白名单不是“写个中间件就完事”的事。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











