直接用c.clientip()或r.remoteaddr做黑名单判断必然失效,因其返回的几乎总是反向代理内网地址而非用户真实出口ip;必须按可信链路降级取值:优先x-real-ip、再x-forwarded-for最左非私有ip(需校验请求来源是否在可信代理网段),最后fallback到remoteaddr并安全拆解;所有ip须经net.parseip解析,网段匹配必须用net.ipnet.contains而非字符串前缀。

直接用 c.ClientIP() 或 r.RemoteAddr 做黑名单判断,线上必然失效——它返回的几乎总是反向代理(Nginx/Cloudflare/ALB)的内网地址,不是用户真实出口 IP。
怎么从 HTTP 请求里安全提取真实客户端 IP
你不能信任任意头字段,必须按可信链路降级取值,并校验来源是否在你可控的代理网段内。
- 优先读
r.Header.Get("X-Real-IP"):Nginx 默认通过proxy_set_header X-Real-IP $remote_addr设置,只设一次,伪造成本高 - fallback 到
r.Header.Get("X-Forwarded-For"):取最左非私有 IP(不是第一个!),前提是请求来源 IP 在你配置的可信代理段内(如"192.168.0.0/16"或阿里云 SLB 的"100.64.0.0/10") - 最后才用
r.RemoteAddr:需先调net.SplitHostPort拆解,再用net.ParseIP提取纯 IP;仅限直连调试场景 - 所有字符串 IP 必须经
net.ParseIP解析后再使用;未解析就比对或传给Contains会静默失败
黑名单数据结构选 map[string]struct{} 而不是 []string 或 sync.Map
万级以下条目,map[string]struct{} 是唯一合理选择:O(1) 查询、零 GC 压力、内存最小、热更新简单。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 初始化:
blacklist := make(map[string]struct{}) - 加载单个 IP:
blacklist["192.168.1.100"] = struct{}{} - 判断是否命中:
_, blocked := blacklist[ip.String()] - 别用
[]string遍历查找——1000 条就要平均 500 次字符串比较,QPS 上千时毛刺明显 - 别用
sync.Map——除非你每秒增删上千次,否则它比普通map+sync.RWMutex更慢、更占内存
支持 CIDR 网段时必须用 net.IPNet.Contains,不能字符串匹配
strings.HasPrefix 在 IPv6 下完全不可靠,且无法处理掩码对齐;net.IPNet.Contains 是标准库唯一正确方式。
- 预解析黑白名单:对每个 CIDR 字符串(如
"192.168.1.0/24"或"2001:db8::/32")调net.ParseCIDR,缓存为*net.IPNet - 单 IP 输入(如
"10.0.0.5")需补成"10.0.0.5/32"再解析;IPv6 单 IP 补/128 - IPv4 和 IPv6 网段必须分开存、分开查:
denyV4 []*net.IPNet和denyV6 []*net.IPNet,混用会导致静默不匹配 - 判断逻辑必须短路:先遍历
denyV4,命中即返回false;未命中再查denyV6;白名单只在黑名单全不命中时生效
中间件里写响应状态码要显式、干净
封 IP 就是明确拒绝,不是资源不存在,也不是限流;响应体里暴露原因等于教攻击者绕过。
- HTTP 状态码必须用
http.StatusForbidden(403),不是 404 或 429 - 写响应前务必先调
w.WriteHeader(http.StatusForbidden),否则 Go 会自动发 200 - 别在响应体里输出
"Your IP xxx is blocked"这类信息——泄露策略细节 - 清空敏感 Header:
w.Header().Del("Server")、w.Header().Del("X-Powered-By") - 如果用了 Gin,别用
c.Abort()后再写状态码,要显式调c.Status(http.StatusForbidden)
真正容易被忽略的是决策顺序和 CIDR 解析时机:黑名单必须优先于白名单检查,否则配置错误时会被意外放行;而所有 *net.IPNet 必须在启动时或热更新时一次性预解析完成,绝不能在每次请求中重复调 net.ParseCIDR——那会成为 CPU 瓶颈。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










