应优先从x-real-ip取真实ip,其次取x-forwarded-for最后一个非私有ip,最后才用remoteaddr;黑名单宜用map[string]struct{}以兼顾查询效率与内存占用。

Go HTTP中间件怎么拦截特定IP请求
直接用 net/http 的 HandlerFunc 套一层就行,不用引入第三方框架。关键不是“加中间件”,而是「在哪取真实IP」和「怎么比对才不漏掉代理后的请求」。
常见错误是只读 r.RemoteAddr,结果所有请求都显示为反向代理(比如 Nginx)的内网地址;或者只检查 X-Forwarded-For 最开头那个 IP,被客户端伪造就失效。
- 优先从
X-Real-IP取(Nginx 默认设这个,可信度高) - fallback 到
X-Forwarded-For的最后一个非私有 IP(注意:要跳过127.0.0.1、10.0.0.0/8等内网段) - 最后才用
r.RemoteAddr(仅限开发或直连场景)
黑名单数据结构选 map 还是 slice
查得快、内存小、更新少,就用 map[string]struct{};如果黑名单要热更新(比如从配置文件或 DB 定期 reload),且条目不多(map 依然最合适。
别用 []string 遍历查找——1000 个 IP 就要平均 500 次字符串比较,QPS 上千时毛刺明显。也别用 sync.Map,除非你真在运行时高频增删,否则它比普通 map + sync.RWMutex 更慢、更占内存。
立即学习“go语言免费学习笔记(深入)”;
- 初始化:
blacklist := make(map[string]struct{}) - 加载时:
blacklist["192.168.1.100"] = struct{}{} - 判断:
_, blocked := blacklist[ip]
HTTP 403 返回前要不要记录日志
要,但别每条都打全量日志。高频攻击下,日志 I/O 会拖慢整个 handler,甚至把磁盘打满。
建议按 IP 聚合计数 + 采样输出:同一 IP 1 分钟内第 1 次拦截记 INFO,之后只记 WARN 并带计数;连续 10 次以上就触发告警(比如发到 Prometheus 或 Slack)。
- 避免在 handler 里调
log.Printf直接写磁盘 - 用结构化日志库(如
zap)的With加字段,比如ip、ua、path - 不要记录 POST body,容易泄露敏感数据,也增大日志体积
为什么本地测试总绕过黑名单
因为 localhost、127.0.0.1 经常被当成可信源跳过检查,或者你在 curl 里没带 X-Real-IP 头,导致中间件取到的是 ::1:port 这种格式,而黑名单里存的是 127.0.0.1 ——两者不等价。
- 测试时用真实局域网 IP(如
192.168.x.x)或容器网络 IP - curl 测试命令示例:
curl -H "X-Real-IP: 192.168.1.200" http://localhost:8080/api - 代码里做 IP 标准化:用
net.ParseIP解析再转字符串,能统一::1和127.0.0.1的表现形式
真正难的不是写几行拦截逻辑,而是搞清你的流量路径里到底有几个代理层、每个层改了哪些头、哪些头你能信——这决定了你取 IP 的顺序和校验方式。漏一层,黑名单就形同虚设。










