必须用 http.MaxBytesReader 在 Gin handler 开头包装 r.Body 以防御表单 DDoS;同时全局禁用 Gin 自动解析 multipart 表单,改由开发者显式控制。

单纯靠 Gin 中间件做请求体大小限制,拦不住表单类 DDoS 攻击;必须在 http.Request 解析前就掐断大 payload,否则内存和 goroutine 早就爆了。
表单提交阶段的 Body 大小必须用 http.MaxBytesReader 包装
攻击者发一个 500MB 的 multipart/form-data 表单,Gin 的 c.PostForm 或 c.MultipartForm 还没开始解析,Go 的 net/http 就已把整个 body 读进内存——触发 GC 风暴甚至 OOM。这不是 Gin 能控制的时机。
正确做法是在 handler 开头、任何 Gin 方法调用前,立刻包装 r.Body:
func uploadHandler(c *gin.Context) {
// 必须放在最前面!
limitReader := http.MaxBytesReader(c.Writer, c.Request.Body, 5*1024*1024) // 5MB
c.Request.Body = limitReader
<pre class="brush:php;toolbar:false;">// 后续才能安全调用 PostForm / MultipartForm
file, err := c.FormFile("file")
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid form"})
return
}
// ...}
-
http.MaxBytesReader是标准库原生支持的流式截断,不缓存完整 body - 值设太小(如 1MB)会误杀正常大文件上传;设太大(如 100MB)起不到防护作用;建议按业务最大合理值 +20% 设定
- 不能只在 upload 路由加——所有接收表单的路由(登录、注册、反馈)都得加,因为攻击者会轮番打
gin.Engine 全局禁用自动解析 multipart 表单
Gin 默认对所有 POST/PUT 请求自动调用 ParseMultipartForm,哪怕你根本不用 c.FormFile。这等于给攻击者白送一个内存放大器。
关掉它,改由开发者显式控制:
r := gin.New()
r.NoMethod(func(c *gin.Context) {
c.AbortWithStatus(http.StatusMethodNotAllowed)
})
// 关键:禁用自动解析
r.Use(func(c *gin.Context) {
if c.Request.Method == "POST" || c.Request.Method == "PUT" {
// 不调用 c.Request.ParseMultipartForm(32
- 关闭后,
c.PostForm和c.MultipartForm会返回空或 error,必须先手动c.Request.ParseMultipartForm - 配合
http.MaxBytesReader使用时,务必在ParseMultipartForm前完成包装 - 这个开关不是性能优化,是安全必需——否则限流中间件再严也拦不住内存耗尽
IP 级限流必须提取纯 IP,且不能复用同一个 rate.Limiter
表单接口(尤其是登录、注册)是 DDoS 重灾区。但直接用 r.RemoteAddr 当 key,会导致同一客户端因端口变化被识别为多个 IP;而全局共用一个 rate.Limiter,等于所有 IP 共享桶,攻击者换 IP 就绕过。
正确实现要同时满足三点:
- 从
r.RemoteAddr提取纯 IP:strings.Split(r.RemoteAddr, ":")[0] - 用
sync.RWMutex保护map[string]*rate.Limiter,避免并发写 panic - 敏感路径(如
/login)用紧阈值:rate.NewLimiter(rate.Every(1*time.Second), 2)(每秒最多 2 次)
别在中间件里每次 new 一个 rate.Limiter——内存泄漏 + 锁争抢;也别用 gin.Context 存 limiter 实例——生命周期不对,容易复用错。
HTTP Server 层必须显式设 IdleTimeout,否则 Slowloris 直接生效
攻击者发一个 POST /login 请求头后,就停住不发 body,连接一直挂着。Gin 中间件永远等不到 c.Request.Body,http.Server 若没设 IdleTimeout,这个连接会卡死 30 分钟以上,迅速吃光 net.Conn 和 goroutine。
启动服务时必须显式配置:
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second, // 关键!不设就是 0(永不超时)
}
srv.ListenAndServe()
-
IdleTimeout是防 Slowloris 的唯一有效手段,和 Nginx 的keepalive_timeout必须错开(比如 Nginx 设 60s,Go 设 30s) - 如果用了反向代理(如 Nginx),还要在 proxy 配置里加
proxy_read_timeout 25;,确保超时链路一致 - 这个配置不在 Gin 里,而在
http.Server实例上——很多人漏掉,结果中间件写得再密也没用
真正难的不是写几行限流代码,而是把 http.MaxBytesReader 插进请求生命周期最早的位置、把 IdleTimeout 设对、把 IP 提取逻辑抠准——这三个点一旦漏掉任意一个,表单 DDoS 就能穿透进来。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











