RemoteAddr不可靠,因它仅返回直连代理(如Nginx、CDN或SLB)的内网IP,而非用户真实IP;正确做法是校验可信代理后解析X-Forwarded-For最左非私有IP或X-Real-IP。

为什么 net/http 的 Request.RemoteAddr 不可靠
直接拿 Request.RemoteAddr 当真实 IP 用,基本等于放行所有代理流量。它返回的是 TCP 连接的远端地址,经过 Nginx、CDN 或云 WAF 后,这里只剩下游负载均衡器的内网 IP(比如 10.0.1.5:42391),原始客户端 IP 已丢失。
正确做法是逐级检查可信的 HTTP 头字段,且必须限定信任的上游代理段。常见组合是:X-Forwarded-For(逗号分隔的 IP 链) + X-Real-IP(单个 IP,常由 Nginx 设置)。但这两个头可被客户端伪造,所以必须配合 X-Forwarded-By 或已知代理 IP 白名单做校验。
实操建议:
- 在入口 middleware 中封装一个
GetClientIP(*http.Request) string函数,不依赖框架自带的“智能 IP 提取”(如 Gin 的c.ClientIP()默认未校验代理链) - 只信任来自内网段(如
10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)或云厂商明确文档列出的回源段(如阿里云 SLB 是100.64.0.0/10)的请求头 - 若
X-Forwarded-For存在且请求来源 IP 在可信代理段内,取其最右非私有 IP;否则回落到RemoteAddr的 IP 部分(并剔除端口)
Gin 中拦截恶意 UserAgent 的实际写法
靠正则硬匹配 User-Agent 字符串效率低、易误杀,也挡不住动态 UA 变种。更可行的是维护一个轻量级黑名单 + 模糊特征规则,配合快速哈希判断。
立即学习“go语言免费学习笔记(深入)”;
实操建议:
- 把高频恶意 UA 特征(如
sqlmap、nikto、gobuster、masscan)存为 map[string]struct{},用strings.Contains前先查表,避免全量正则编译开销 - 对 UA 做小写 normalize(
strings.ToLower),再比对;注意不要用strings.ToLower处理整个请求体,只处理r.Header.Get("User-Agent") - 拦截响应统一返回
http.StatusForbidden,但不要暴露原因(避免泄露防御逻辑),Header 中清空Server和X-Powered-By - 加一个开关配置项
enableUAFilter bool,上线前先设为 false 并打日志,观察一周再开启阻断
IP 黑名单与速率限制共用时的坑
很多实现把 IP 封禁和限流放在两个独立中间件里,结果出现:IP 已在黑名单中,却仍被计入限流计数器,导致 Redis key 泛滥或误触发限流阈值。
实操建议:
- 把 IP 黑白名单校验放在速率限制 middleware 之前,且一旦命中黑名单,立即 return,不调用 next()
- 使用原子操作判断:先查黑名单(本地 map 或 Redis SET),命中则直接响应;未命中再进限流逻辑(如基于
github.com/ulule/limiter+ Redis store) - 黑名单本身建议用 Redis Sorted Set 存过期时间(score = unix timestamp),避免内存泄漏;不要用纯内存 map,重启即失效
- 注意 IPv6 地址格式差异——
::1、2001:db8::1等需归一化为标准压缩格式再比对,否则相同 IP 可能因格式不同漏判
如何验证拦截逻辑是否生效
光写代码不验证,等于没写。真实环境中,拦截点经常被反向代理吞掉头、被 CDN 缓存跳过、或因 TLS 终止位置不对导致 UA/IP 错位。
实操建议:
- 用 curl 手动构造测试请求:
curl -H "X-Forwarded-For: 1.2.3.4, 10.0.1.10" -H "User-Agent: sqlmap/1.7" http://localhost:8080/api/test,观察响应状态码和日志 - 在拦截 middleware 中加一行 debug 日志:
log.Printf("IP=%s, UA=%q, matchedBlacklist=%v", ip, ua, blacklisted),但上线前必须关闭(或用 zap 的 level 控制) - 部署后立刻查 Nginx access log,确认
$http_x_forwarded_for和$remote_addr是否符合预期;若全是内网 IP,说明 Go 服务没配对 X-Forwarded-For 解析逻辑 - 别忽略边缘 case:HTTP/2 请求可能携带重复头、某些爬虫会发空 UA(
"")、IPv6 地址带端口([::1]:34221)需提前截断
真正难的不是写拦截,而是让每层基础设施(CDN、WAF、LB、Go 服务)对“谁是客户端”达成一致。IP 和 UA 都是易伪造字段,所有校验都得建立在可信边界之上——这个边界,得你亲手划清楚。


















