Gin 本身不内置防爬虫限流,必须靠中间件组合实现,核心是 golang.org/x/time/rate + getRemoteIp + 动态 key 构造;令牌桶优于固定窗口因其避免窗口切换边缘效应、允许合理突发且长期平滑;需正确提取真实 IP(优先 X-Forwarded-For 并过滤私有地址)、分层设计 key(user/device/ip)、限流中间件须置于认证之后业务之前,并返回标准 429 响应头。

直接说结论:Gin 本身不内置防爬虫限流,必须靠中间件组合实现,核心是 golang.org/x/time/rate + getRemoteIp + 动态 key 构造,否则容易被绕过或误杀。
为什么用令牌桶而不是固定窗口?
固定窗口(比如“每分钟最多100次”)在窗口切换时存在边缘效应——攻击者可在每分钟开头集中发请求,瞬间打满额度;而令牌桶允许短时突发但长期平滑,更贴合真实用户行为和防御需求。
-
rate.Limiter的Allow或Wait方法能精确控制单个请求是否放行 - burst 参数要设合理:太小会误伤正常用户(如页面加载触发多个 API),太大则失去限流意义
- rate 值建议从 10 QPS 起调,结合日志观察真实流量分布再逐步收紧
如何正确提取客户端真实 IP?
直接用 c.Request.RemoteAddr 在有 CDN、Nginx 反向代理时会拿到代理 IP,导致所有用户共享一个限流桶,完全失效。
- 必须使用类似 S-UI 中的
getRemoteIp函数:优先读X-Forwarded-For头,取第一个非私有地址 - 私有网段(如
10.0.0.0/8、127.0.0.1)需过滤,否则内部服务调用也会被计入限流 - 若用 Cloudflare,还需检查
Cf-Connecting-Ip头作为 fallback
限流 key 怎么设计才不被绕过?
只按 IP 限流容易被代理池绕过;只按用户 ID 限流又对未登录接口无效。实际应分层构造 key:
- 已登录用户:
"user:" + userID(优先级最高) - 未登录但有可信设备指纹:
"device:" + hash(ua+ip+screen)(需前端配合传参) - 兜底 IP 级:
"ip:" + cleanIP(必须带私有地址过滤) - 避免用
Host或Referer做 key —— 容易被伪造
中间件里怎么处理被限流的请求?
简单返回 429 Too Many Requests 不够,得兼顾体验和反探测:
- 响应头必须加
X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset,方便前端判断 - 不要暴露具体限流策略(如 burst 值),避免给攻击者提供试探依据
- 对高频恶意 IP,可额外写入 Redis 黑名单,后续直接
c.AbortWithStatus(http.StatusForbidden) - 日志中记录被限流的
key和原始X-Forwarded-For,用于事后分析爬虫特征
最容易被忽略的是:限流中间件必须放在 checkLogin 之后、业务 handler 之前 —— 否则未登录用户的请求根本进不到限流逻辑里,而登录中间件又可能因数据库查询慢拖垮整个链路。顺序错了,再精细的算法也白搭。


















