应在Echo中间件中优先从X-Real-IP或X-Forwarded-For提取真实IP,结合可信代理配置校验;白名单用map或预解析的*net.IPNet支持单IP与CIDR匹配,黑名单优先快速拒绝,全局Use确保覆盖所有路由。

如何在 Echo 中拦截非白名单 IP 的请求
直接在 echo.MiddlewareFunc 里读取客户端真实 IP,比对预设白名单切片,匹配失败就返回 403 Forbidden。注意别用 c.Request().RemoteAddr,它返回的是代理后端地址(如 127.0.0.1:5678),不是用户真实 IP。
真实 IP 需从 X-Forwarded-For 或 X-Real-IP 头中提取,但必须信任上游代理——否则攻击者可伪造头绕过。若服务直连公网(无 Nginx/Cloudflare),c.RealIP() 就够用;若有反向代理,需提前配置 e.IPExtractor = echo.ExtractIPFromXFFHeader 或自定义提取逻辑。
- 白名单建议存为
map[string]struct{},O(1) 查找,避免每次遍历切片 - IP 字符串要标准化:IPv4 去掉前导零(
"192.168.001.001"→"192.168.1.1"),IPv6 要归一化("2001:db8::1") - 测试时用
curl -H "X-Forwarded-For: 1.2.3.4" http://localhost/模拟带头请求
如何支持 CIDR 网段匹配(比如 192.168.0.0/16)
纯字符串匹配无法处理网段,得用标准库 net 包。把白名单条目解析成 *net.IPNet,再用 ipNet.Contains(clientIP) 判断。
示例初始化代码:
立即学习“go语言免费学习笔记(深入)”;
var ipNets []*net.IPNet
for _, cidr := range []string{"192.168.0.0/16", "2001:db8::/32"} {
_, ipNet, err := net.ParseCIDR(cidr)
if err == nil {
ipNets = append(ipNets, ipNet)
}
}
- 解析失败的 CIDR 会被静默跳过,建议启动时校验并 panic 或 log.Fatal
- IPv4 和 IPv6 地址需分别解析,
net.ParseIP("::1")返回 IPv6 地址,net.ParseIP("127.0.0.1")返回 IPv4,别混用IP.To4()强转 - 如果白名单同时含单 IP 和 CIDR,统一转成
*net.IPNet更省事:/32和/128即可覆盖
黑名单优先级是否高于白名单?怎么组合使用
黑白名单不是“非此即彼”,而是执行顺序决定效果。典型做法是:先查黑名单,命中则立即拒绝;再查白名单,未命中则拒绝。中间不短路就失去意义。
- 黑名单应走快速失败:用
map[string]struct{}存单 IP,或预解析*net.IPNet存网段 - 不要把黑白名单塞进同一个 map——语义混淆,后期维护容易漏逻辑分支
- 若需动态更新(如从 DB 加载),用
sync.RWMutex保护读写,但注意频繁写入会阻塞请求中间件 - 日志记录被拦截的 IP 时,务必脱敏或限频,防止日志膨胀或信息泄露
为什么加了中间件却对 /healthz 这类路径没生效
因为 Echo 的 Group 或路由注册顺序导致中间件未覆盖目标路径。最常见情况是:你给 e.GET("/api/*", ...) 加了过滤,但 /healthz 是单独注册在根 e 上的,没经过该中间件链。
- 全局启用:用
e.Use(ipFilterMiddleware),确保所有路由都走一遍 - 局部启用:确认
/healthz所在的 Group 或路由是否显式调用了Use() - 排除特定路径:在中间件里手动跳过,例如
if c.Path() == "/healthz" { return next(c) },而不是依赖路由分组 - 调试技巧:在中间件开头加
log.Printf("IP filter on %s from %s", c.Path(), clientIP),确认是否被调用


















