gin 的 net/http 默认中间件挡不住爬虫,因其仅返回 html 而不校验 content-type、user-agent 或限速;爬虫只认状态码和响应体,且 ua 易伪造,需结合 referer、x-requested-with、accept 头及动态漏桶限速与 js 挑战协同防御。

为什么 Gin 的 net/http 默认中间件挡不住爬虫
因为爬虫根本不在意你返回的 HTML 渲染效果,它只认 HTTP 状态码和响应体内容。Gin 默认的 gin.Default() 里连 Content-Type 都没强制校验,更别说识别 User-Agent 或限速了。很多开发者加了个 if strings.Contains(c.Request.UserAgent(), "curl") 就以为防住了,结果被 Python 的 requests(默认 UA 是 python-requests/2.*)或带随机 UA 的 Scrapy 一穿就透。
用 gin.HandlerFunc 实现基础请求指纹过滤
核心不是“封 UA”,而是建立可验证的请求上下文。真实用户有 Cookie、会带 Referer、JS 执行后会发带 X-Requested-With: XMLHttpRequest 的请求;爬虫通常缺失其中 2–3 项。建议按以下顺序判断:
- 检查
c.Request.Header.Get("Referer")是否为空或域名不匹配(比如接口只允许来自example.com) - 检查
c.Request.Header.Get("X-Requested-With")是否为"XMLHttpRequest"(AJAX 请求才有) - 检查
c.Request.Header.Get("Accept")是否包含"application/json"(纯 API 场景下,浏览器页面请求通常是text/html) - 对无 Referer 且 Accept 不是 JSON 的请求,直接
c.AbortWithStatusJSON(403, gin.H{"error": "Forbidden"})
注意:不要单独依赖 User-Agent 字符串匹配,它太容易伪造;也不要只靠 IP 限频,CDN 或代理池会让单 IP 请求量看起来完全正常。
用 golang.org/x/time/rate 做动态请求限速
固定窗口限频(如“每分钟最多 60 次”)会被爬虫在窗口边界集中打爆。改用漏桶算法更稳:
import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(rate.Limit(5), 10) // 平均每秒 5 次,最多突发 10 次
func RateLimitMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !limiter.Allow() {
c.AbortWithStatusJSON(429, gin.H{"error": "Too Many Requests"})
return
}
c.Next()
}
}
关键点:
- 把
limiter定义成包级变量,避免每次请求都新建实例 - 突发容量(第二个参数)设为平均速率的 2–3 倍,否则正常用户快速点击也会被拦
- 如果业务需要区分登录/未登录用户,得按
c.ClientIP()或c.GetString("user_id")分 key 管理多个rate.Limiter
前端配合:用 Set-Cookie + JS 挑战绕过静态爬虫
纯服务端规则对无头浏览器(Puppeteer、Playwright)无效。必须让前端参与——首次访问返回一个带 HttpOnly=false 的 Cookie(比如 js_challenge=1),并在页面内执行一段 JS 计算哈希写入另一个 Cookie(如 js_proof=sha256(nonce+ua+ts))。后端接口只接受同时携带这两个 Cookie 且 js_proof 校验通过的请求。
这样做不是为了防住高级爬虫,而是抬高门槛:静态爬虫拿不到 js_proof,无头浏览器要额外注入 JS 才能生成,而多数批量拖库脚本不会做这一步。
真正难处理的是用真实浏览器集群跑的分布式爬虫——这时候就得上行为分析(鼠标轨迹、滚动节奏)或对接风控 SDK,那已经超出 Gin 中间件能解决的范围了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











