不能直接用c.request().useragent()简单匹配,因ua可伪造、易误杀;应采用“拒绝已知恶意ua+对无/极简ua限流”组合策略,并确保中间件注册在e.use()链靠前位置、先过滤再日志、ua需清洗后匹配、与ip限流协同时共享ip桶。

为什么不能直接用 c.Request().UserAgent() 做简单匹配
因为 User-Agent 字段可被任意伪造,中间件里只做字符串相等或 strings.Contains 判断,既拦不住真实爬虫(比如用 curl 伪装成 Chrome),又容易误杀合法用户(比如新版 Chrome 更新 UA 后缀导致匹配失败)。真正需要的是「明确拒绝已知恶意 UA」+「对无 UA 或极简 UA 主动限流」的组合策略。
middleware.UserAgentFilter 的正确注册时机和位置
必须注册在 e.Use() 链中、且早于所有业务中间件(如 JWT、日志、CORS),否则可能被后续中间件提前返回响应而绕过。它不能放在路由组(e.Group())里——UA 过滤是全局前置守门员,不是某类接口的专属逻辑。
- 错误写法:
apiGroup.Use(userAgentMiddleware)→ 管理后台 /admin 路由就完全不受控 - 正确写法:
e.Use(userAgentMiddleware),紧接在e.Use(middleware.Recover())之后 - 别和
middleware.Logger顺序颠倒:先过滤再记日志,避免恶意请求刷满日志文件
如何安全提取并标准化 UA 字符串
注意 c.Request().UserAgent() 返回值可能为空(curl -A "")、含换行或控制字符,直接用于 switch 或 map 查找会 panic 或漏判。必须先清洗:
- 用
strings.TrimSpace()去首尾空格和 \r\n - 长度为 0 时统一视为可疑,建议归入「无 UA」桶单独限流
- 避免用
strings.ToLower()全转小写——某些 UA 含 base64 片段,大小写敏感 - 不依赖正则预编译全局变量,每个请求新建
regexp.MustCompile会拖慢性能;改用strings.HasPrefix或白名单map[string]struct{}
示例关键判断逻辑:
ua := strings.TrimSpace(c.Request().UserAgent())
if ua == "" {
// 触发 IP 级限流(复用防刷中间件里的 sync.Map)
return handleSuspiciousUA(c)
}
if blockedUAs[ua] || strings.HasPrefix(ua, "sqlmap") || strings.Contains(ua, "Nikto") {
return c.String(403, "Forbidden")
}
与 IP 限流联动时的常见坑
单纯封 UA 意义有限,必须和 IPBasedRateLimit 协同。但要注意两个中间件的状态隔离问题:
- 不能让 UA 过滤中间件自己 new 一个
rate.Limiter—— 每次请求都新建等于没限 - 也不能复用同一个限流器实例 —— 不同 UA 的请求应共享 IP 桶,而不是各自建桶
- 正确做法:UA 中间件只做标记(如
c.Set("ua_blocked", true)),由下游限流中间件读取该标记后主动降权(比如把该 IP 的 QPS 从 10 降到 1) - 更稳妥的是在 UA 中间件里直接调用已有的限流器
Allow(),但 key 必须仍是 IP,不能拼接 UA 字符串(否则 Redis Key 爆炸)
生产环境上线前务必验证:curl -A "python-requests/2.0" 会被拦截,但 curl -A "Mozilla/5.0 (X11; Linux x86_64)..." 必须放行——这个边界稍不注意就会变成“全放行”或“全拦截”。











