Files
cs-note/hzh/GIN/12-server-config.md
2026-05-24 11:42:38 +08:00

5.8 KiB
Raw Permalink Blame History

tags, create time
tags create time
后端
Go
Gin
服务器
部署
2026-04-28 00:00

服务器配置与安全

概述

gin.New() 创建的是一个不带任何中间件的 bare Engine,配合标准库 http.Server 可以实现完全可控的服务启动。这一节涵盖高级服务器配置(超时、TLS、KeepAlive)、Cookie 操作、以及可信代理链的安全考量。

思考题:为什么在生产环境中不能依赖 gin.Default() 和 r.Run() 启动服务?生产环境应该用什么样的 http.Server 配置?

正文

1. 自定义 Engine + http.Server 启动

func main() {
    // 不使用 Default(无自动 Logger/Recovery)
    r := gin.New()

    // 手动添加需要的中间件
    r.Use(gin.Recovery())
    r.Use(requestID())
    r.Use(logger())

    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })

    // 完整控制 http.Server
    srv := &http.Server{
        Addr:         ":8080",
        Handler:      r,              // Gin Engine 作为 Handler
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
        MaxHeaderBytes: 1 << 20,      // 1MB header 限制
    }

    log.Fatal(srv.ListenAndServe())
}

超时配置说明:

超时项 作用 建议值 风险
ReadTimeout 整个请求体读取时间 5-10s 不设 → slowloris 攻击
WriteTimeout 响应写入时间 10-30s 不设 → 长连接耗尽
IdleTimeout KeepAlive 空闲等待 60-120s 不设 → goroutine 泄漏
MaxHeaderBytes 请求头最大大小 1MB 不设 → 默认 1MB

提问: ReadTimeout 是从连接建立开始计时,还是从最后一个字节读完开始?如果客户端发送一个巨型请求头(比如 100KB),ReadTimeout 会生效吗?

Gin 封装了便捷的 Cookie 读写方法:

func setCookie(c *gin.Context) {
    // 设置 Cookie
    c.SetCookie(
        "session_id",           // name
        "abc123xyz",            // value
        3600,                   // maxAge (秒)
        "/",                    // path
        "example.com",          // domain (空 = 当前域名)
        true,                   // secure (HTTPS only)
        true,                   // httpOnly (JS 不可访问)
    )
    c.JSON(200, gin.H{"message": "cookie set"})
}

func getCookie(c *gin.Context) {
    cookie, err := c.Cookie("session_id")
    if err != nil {
        c.JSON(400, gin.H{"error": "no cookie"})
        return
    }
    c.JSON(200, gin.H{"session_id": cookie})
}

Cookie 安全标志组合:

secure httpOnly SameSite 适用场景
true true Strict 认证 Cookie(最高安全)
true true Lax 会话 Cookie(平衡安全与体验)
false true Lax 开发环境

3. TLS / Let's Encrypt

Gin 本身不做 TLS 终止——你通过标准库 http.Server 配置:

srv := &http.Server{
    Addr:    ":443",
    Handler: r,
}

// 方式一:已知证书
log.Fatal(srv.ListenAndServeTLS("/path/to/cert.pem", "/path/to/key.pem"))

// 方式二:Let's Encrypt (acme.AutoHTTPS)
// 最简单的方式——不需要证书文件
srv := &http.Server{
    Addr:    ":443",
    Handler: r,
}
// acme 包会在首次请求时自动申请证书
r.RunTLS("", "", "") // Gin 快捷方式,等价于 ListenAndServeTLS("", "")

Gin 的 RunTLS 快捷方法:

// ListenAndServeTLS 的简化
r.RunTLS(":443", "/cert.pem", "/key.pem")  // 指定文件和端口
r.RunTLS(":443", "", "")                     // ACME 自动 HTTPS

4. 可信代理链(Trusted Proxies)

当 Gin 跑在 Nginx/Cloudflare/K8s Ingress 后面时,c.ClientIP() 默认拿到的是负载均衡器的内网 IP,不是真实用户 IP:

func main() {
    r := gin.New()

    // 告诉 Gin 哪些 IP 是可信的代理
    gin.SetMode(gin.ReleaseMode)
    gin.DefaultSkipPanicHandler = true

    // 标记所有内网 IP 为可信代理
    gin.SetTrustedProxies([]string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"})

    // 或在运行时
    r.Use(func(c *gin.Context) {
        c.SetTrustedProxyFn(func(remoteAddr net.Addr) bool {
            ip := remoteAddr.(*net.TCPAddr).IP
            return ip.IsPrivate() || ip.Equal(net.IPv4loopback)
        })
    })

    // 现在 c.ClientIP() 会正确解析 X-Forwarded-For / X-Real-IP
    r.GET("/hello", func(c *gin.Context) {
        ip := c.ClientIP()
        c.String(200, "Hello from %s", ip)
    })

    r.Run()
}

安全警告: 如果不设置可信代理,攻击者可以伪造 X-Forwarded-For 头注入任意 IP,绕过 IP 白名单限流。务必只信任你知道的代理 IP 段。

5. 完整的生产环境启动规范

func main() {
    r := setupRouter()

    srv := &http.Server{
        Addr:         ":" + os.Getenv("PORT"),
        Handler:      r,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  120 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }

    // 优雅关闭(见 [[13-graceful-shutdown]])
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("server error: %v", err)
        }
    }()

    // 阻塞主 goroutine
    select {}
}

思考题:如果你的服务同时接收短连接(HTTP/1.1)和长连接(HTTP/2),IdleTimeout 对两种协议的行为一样吗?什么情况下它会失效?

关联笔记