最轻量可控的方式是用闭包中间件在HTTP handler前拦截请求,需正确提取真实IP(优先X-Forwarded-For/X-Real-IP,Fallback RemoteAddr并剥离端口),黑名单优先匹配,白名单默认拒绝,配置应外置而非硬编码,小规模用map[string]struct{},高频更新需考虑sync.RWMutex保护的map。

如何用 net/http 中间件实现 IP 黑白名单过滤
直接在 HTTP handler 前拦截请求是最轻量、最可控的方式。Golang 标准库的 net/http 不带内置黑白名单,但用一个闭包中间件就能搞定,无需引入第三方框架。
常见错误是把 IP 判断写在业务 handler 里,导致每个接口都要重复校验;或者误用 req.RemoteAddr——它可能带端口(如 "192.168.1.100:54321"),不剥离会匹配失败。
- 始终用
strings.Split(req.RemoteAddr, ":")[0]或net.ParseIP()提取纯 IP - 黑名单优先于白名单:命中黑名单直接
http.Error(w, "Forbidden", http.StatusForbidden) - 白名单模式下,不在列表中的 IP 应统一拒绝,不要漏掉
localhost或127.0.0.1这类调试常用地址 - 若服务部署在 Nginx 或云 LB 后,真实 IP 在
X-Forwarded-For头里,需额外解析且注意伪造风险
sync.Map vs map[string]bool:高频更新时怎么选
黑白名单通常需要运行时动态增删(比如运营后台触发封禁),map[string]bool 虽简单,但并发读写会 panic;而 sync.Map 虽线程安全,但遍历性能差、不支持原子性批量操作。
实际建议:小规模名单(sync.RWMutex + 普通 map;大规模且更新频繁时,才考虑 sync.Map,但要注意它的 Load/Store 接口返回的是 interface{},需类型断言。
立即学习“go语言免费学习笔记(深入)”;
- 初始化时预分配容量:
ips := make(map[string]bool, 256)减少扩容开销 - 读多写少场景下,
RWMutex的读锁几乎无开销,比sync.Map更快 - 避免在每次请求中调用
mutex.Lock()—— 只在管理接口(如/admin/block)中加锁,校验逻辑全程只读
如何支持 CIDR 网段匹配(如 "192.168.0.0/16")
纯字符串匹配只能处理单 IP,生产环境必须支持网段。Golang net 包原生支持,但容易忽略 IPv4/v6 双栈兼容问题。
关键点在于:用 net.ParseCIDR(cidrStr) 解析网段,再用 *net.IPNet.Contains(ip) 判断,而不是手动拆分掩码位数。
- IPv6 地址需标准化为全格式(如
"2001:db8::1"),否则ParseCIDR可能失败 - 同一份配置里混用
"10.0.0.0/8"和"::1"时,要分别解析,不能共用一个*net.IPNet - 网段列表不宜过大(>100 条),否则每次请求都遍历所有网段影响延迟;可按前缀长度排序,优先匹配更精确的(如 /32 > /24)
为什么不应该把黑白名单硬编码在代码里
硬编码意味着每次修改都要重新编译、发版,违背微服务“配置即代码”的基本要求。哪怕只是临时测试,也应从环境变量或配置文件加载。
典型错误是用 flag.String 或全局常量定义名单,结果上线后发现无法热更新,只能重启网关——这对高可用网关是不可接受的。
- 推荐方式:启动时读取 YAML/JSON 配置文件,结构类似
{"blacklist": ["1.2.3.4", "192.168.0.0/16"]} - 进阶需求(如实时生效)需监听文件变更或接入 Consul/Etcd,但注意 watch 机制本身有延迟,不能替代严格鉴权
- 配置加载失败应 panic 或 abort 启动,别静默降级——黑白名单失效等于裸奔
真正难的不是写过滤逻辑,而是搞清流量路径上谁提供真实 IP、网段规则是否覆盖了所有出口节点、以及配置变更时如何不丢请求。这些细节没对齐,代码写得再漂亮也没用。


















