直接用 map[string]bool 做白名单会线上失效,因其不支持 cidr 网段、并发读写未加锁易 panic 或漏判,且热更新替换 map 会导致中间件请求看到新旧不一致状态。

为什么直接用 map[string]bool 做白名单会线上失效
因为 map 不支持 CIDR 网段(如 10.20.0.0/16),且并发读写时未加锁会导致 panic 或漏判;更隐蔽的是热更新时直接替换整个 map,中间件正在执行的请求可能看到新旧不一致的状态。
- 用
net.IPNet+IPSet(如github.com/deckarep/golang-set的变体或原生net包)替代 map,支持/24、/16等网段匹配 - 把规则容器包一层
atomic.Value,更新时调用Store(),读取时用Load().(*IPSet),避免锁竞争 - 初始化阶段从配置中心或文件加载后,必须一次性构建完整
IPSet实例再存入atomic.Value,禁止边遍历边 Add
Gin 中间件里怎么拿到真实客户端 IP 而不是 127.0.0.1
c.ClientIP() 默认只信任本地回环,线上反向代理(Nginx/Traefik/CDN)下必然返回代理内网地址。必须配合可信代理链解析 X-Forwarded-For 或 X-Real-IP,且显式声明哪些网段是可信的。
- 在 Gin engine 初始化后立即调用
engine.SetTrustedProxies([]string{"192.168.0.0/16", "10.0.0.0/8", "203.208.60.0/24"}),否则c.ClientIP()仍不可靠 - 若明确由 Nginx 设置了
X-Real-IP,优先用c.Request.Header.Get("X-Real-IP");若走多层代理,用c.Request.Header.Get("X-Forwarded-For")并取最左非信任地址(需手动切分和校验) - 永远不要在没配
SetTrustedProxies的情况下依赖c.ClientIP(),生产环境基本等于裸奔
如何让白名单豁免健康检查和内部调试路径
硬编码 if path == "/healthz" || path == "/debug/pprof" 不可维护,且容易遗漏新接入的探针路径。应该把豁免规则外置为配置项,并在中间件开头统一判断。
- 定义一个全局豁免路径列表:
var exemptPaths = []string{"/healthz", "/readyz", "/metrics", "/debug/"} - 中间件中先做前缀匹配或全等判断:
for _, p := range exemptPaths { if strings.HasPrefix(path, p) || path == p { c.Next(); return } } - 如果使用 APISIX 或 Nginx 做前置过滤,建议把豁免逻辑下沉到网关层,避免每个服务重复判断
白名单规则变更后如何不重启服务就生效
规则热更新本身不难,难的是确保所有 goroutine 看到的都是同一份最新快照,且更新过程不阻塞请求处理。
- 用
atomic.Value存储*IPSet,更新函数里构造新实例再Store,读取端无锁Load - 监听配置中心变更事件(如 Nacos、Consul、etcd),收到通知后触发规则重载,避免轮询开销
- 务必在重载逻辑里加日志:
log.Printf("whitelist updated: %d IPs, %d networks", ipSet.Cardinality(), len(ipSet.Networks())),方便核对是否真生效











