Gin 中 c.ClientIP() 默认不信任外部代理,需显式配置可信网段并结合 X-Real-IP 或 X-Forwarded-For 获取真实 IP;Nginx 用 geo 模块高效拦截;Spring Boot 应解析请求头剔除内网段,并用 Redis + 本地缓存管理黑白名单;高频动态场景推荐 OpenResty + Redis。

别用 c.ClientIP() 直接取 IP,它在线上基本失效
Gin 的 c.ClientIP() 默认只信任 127.0.0.1 和 ::1,一旦前面有 Nginx、CDN 或 Traefik,返回的就是代理内网地址(比如 10.0.0.3),根本不是用户真实出口 IP。这不是 bug,是设计如此——它不自动信任任何外部代理。
正确做法是显式配置可信代理段,并优先从 X-Real-IP 或 X-Forwarded-For 解析:
- 如果只有一层反向代理(如 Nginx 直连 Go 服务),确保 Nginx 配置了
proxy_set_header X-Real-IP $remote_addr;,然后在 Gin 中用c.Request.Header.Get("X-Real-IP") - 如果有多层(例如 CDN → Nginx → Go),必须用
X-Forwarded-For并取最左侧“非可信地址”:先调用engine.SetTrustedProxies([]string{"192.168.0.0/16", "203.208.60.0/24"})声明可信网段,再调用c.ClientIP()——此时它才真正可靠 - 永远不要直接信任客户端传来的
X-Forwarded-For全字段,攻击者可伪造;SetTrustedProxies的作用就是让 Gin 自动剔除链中可信节点的 IP,只保留最外层不可信来源
Nginx 层拦截比代码层更高效,但别只依赖它
在 Nginx 配置黑白名单,请求根本不到后端,零延迟拦截。适合封禁已知恶意网段或放行运维固定出口 IP。
但要注意两点硬伤:
- 静态配置难更新:每次增删都要 reload Nginx,不适合高频变动的名单(如秒级封禁爬虫)
- 无法结合业务逻辑:比如“只对
/api/v1/sms接口启用白名单”,Nginx 做不到路径级动态策略
典型高效写法(千万级 QPS 场景验证过):
geo $blacklist {
default 0;
include /etc/nginx/ip_blacklist.ranges;
192.168.1.0/24 1;
}
server {
if ($blacklist) { return 403; }
}
注意:geo 模块查表是 O(1),比 if ($remote_addr ~ ...) 正则匹配快一个数量级,超百条规则必须用它。
Spring Boot 拦截器里校验 IP,关键在真实 IP 提取和缓存策略
Spring Boot 实现黑白名单,核心不在“怎么拦”,而在“拦谁”——HttpServletRequest.getRemoteAddr() 同样不可靠,必须自己解析头。
推荐复用成熟工具类(如 Apache Commons 的 RemoteAddrResolver),或手动处理:
- 按顺序检查
X-Real-IP→X-Forwarded-For→remoteAddr,并剔除已知内网段(10.0.0.0/8、127.0.0.0/8、172.16.0.0/12、192.168.0.0/16) - 黑名单用 Redis 的
SET存,白名单用SET或HASH(支持带备注、过期时间) - 避免每次请求都查 DB:本地加 Guava Cache 做二级缓存,
expireAfterWrite(10, TimeUnit.MINUTES),减轻 Redis 压力
示例判断逻辑:
String clientIp = IpUtils.getRealIp(request);
if (redisTemplate.opsForSet().isMember("ip:blacklist", clientIp)) {
throw new AccessDeniedException("Blocked by blacklist");
}
// 白名单优先级更高,需先检查
if (!redisTemplate.opsForSet().isMember("ip:whitelist", clientIp)) {
throw new AccessDeniedException("Not in whitelist");
}
WAF 或 OpenResty + Redis 才适合动态高频场景
当黑白名单每分钟变更数十次(例如自动封禁异常登录 IP),纯 Nginx 静态配置或 Spring Boot 内存缓存都不够用。这时得上带运行时能力的方案。
OpenResty 是性价比最高的选择:
- 用 Lua 脚本读 Redis,毫秒级响应,不穿透到后端
- 支持热更新:Redis 里改数据,OpenResty 立即生效,无需 reload
- 可结合限流模块(
resty.limit.count)做联动策略,比如“单 IP 5 分钟内失败 10 次自动进黑名单”
关键点:Redis key 设计要区分黑白名单,例如:
-
ip:whitelist:192.168.1.100→ value 可设为1或过期时间戳 -
ip:blacklist:203.208.60.12→ value 设为封禁原因和 UTC 时间
别把所有 IP 都塞进一个大 Set,用前缀分片 + TTL 控制内存增长,否则 Redis 内存会失控。
真实部署时,最易被忽略的是代理链信任边界——只要漏配一个可信网段,X-Forwarded-For 就可能被绕过;而最常踩的坑是把 127.0.0.1 加进 SetTrustedProxies,结果本地测试正常,一上生产全崩。

















