TLS/HTTPS

HTTPS 已经是现代 Web 服务的标配了。没有 HTTPS 的网站,浏览器会显示"不安全",搜索引擎排名也会受影响。Gin 启用 HTTPS 其实很简单,但背后的一些细节值得了解。

HTTPS 与 TLS 简介

HTTPS = HTTP + TLS。TLS(Transport Layer Security)是传输层安全协议,它的作用是:

  • 加密:数据传输过程中被加密,中间人无法窃听
  • 身份验证:证书验证服务器身份,防止钓鱼网站
  • 完整性:数据传输过程中被篡改能被检测出来

要启用 HTTPS,你需要一个 SSL/TLS 证书。证书由证书颁发机构(CA)签发,证明你是域名的所有者。

获取证书

开发环境:自签名证书

本地开发可以用 OpenSSL 生成自签名证书:

# 生成私钥和证书(有效期365天)
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365

执行过程中会问你一些信息,随便填就行。生成的 key.pem 是私钥,cert.pem 是证书。

生产环境:Let's Encrypt

Let's Encrypt 提供免费的 SSL 证书,可以用 certbot 工具申请:

# 安装 certbot
sudo apt install certbot

# 申请证书
sudo certbot certonly --standalone -d yourdomain.com

证书会保存在 /etc/letsencrypt/live/yourdomain.com/ 目录下。

Gin 启用 HTTPS

基本方式

最简单的方式是使用 RunTLS 方法:

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    
    router.GET("/", func(c *gin.Context) {
        c.String(200, "Hello HTTPS")
    })

    // 传入证书和私钥路径
    router.RunTLS(":443", "cert.pem", "key.pem")
}

RunTLS 内部其实就是调用 http.ServerListenAndServeTLS 方法。

使用 http.Server 配置

如果需要更多控制,可以自己创建 http.Server

package main

import (
    "crypto/tls"
    "log"
    "net/http"
    
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    
    router.GET("/", func(c *gin.Context) {
        c.String(200, "Hello HTTPS")
    })

    // 自定义 TLS 配置
    cfg := &tls.Config{
        MinVersion:               tls.VersionTLS12,
        CurvePreferences:         []tls.CurveID{tls.X25519, tls.CurveP256},
        PreferServerCipherSuites: true,
        CipherSuites: []uint16{
            tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
            tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
            tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
            tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        },
    }

    srv := &http.Server{
        Addr:      ":443",
        Handler:   router,
        TLSConfig: cfg,
    }

    log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))
}

这个配置指定了最低 TLS 版本为 1.2,并设置了一组安全的加密套件。

HTTP 自动跳转 HTTPS

通常我们希望用户访问 HTTP 时自动跳转到 HTTPS:

package main

import (
    "log"
    "net/http"
    
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    
    router.GET("/", func(c *gin.Context) {
        c.String(200, "Hello HTTPS")
    })

    // 启动 HTTP 服务器,重定向到 HTTPS
    go func() {
        http.ListenAndServe(":80", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently)
        }))
    }()

    // 启动 HTTPS 服务器
    log.Fatal(router.RunTLS(":443", "cert.pem", "key.pem"))
}

这样,访问 http://example.com 会自动跳转到 https://example.com

自动申请 Let's Encrypt 证书

使用 golang.org/x/crypto/acme/autocert 包可以自动申请和续期 Let's Encrypt 证书:

package main

import (
    "crypto/tls"
    "log"
    "net/http"
    
    "github.com/gin-gonic/gin"
    "golang.org/x/crypto/acme/autocert"
)

func main() {
    router := gin.Default()
    
    router.GET("/", func(c *gin.Context) {
        c.String(200, "Hello HTTPS with auto cert")
    })

    // 配置自动证书
    manager := &autocert.Manager{
        Prompt:     autocert.AcceptTOS,
        HostPolicy: autocert.HostWhitelist("example.com", "www.example.com"),
        Cache:      autocert.DirCache("certs"),
    }

    // TLS 配置
    cfg := &tls.Config{
        GetCertificate: manager.GetCertificate,
    }

    srv := &http.Server{
        Addr:      ":443",
        Handler:   router,
        TLSConfig: cfg,
    }

    // HTTP 服务器用于 ACME 验证和重定向
    go func() {
        http.ListenAndServe(":80", manager.HTTPHandler(nil))
    }()

    log.Fatal(srv.ListenAndServeTLS("", ""))
}

这个方案的好处:

  • 首次启动时自动申请证书
  • 证书快过期时自动续期
  • 不用手动管理证书文件

需要注意的是,服务器必须能被 Let's Encrypt 服务器访问到(80 和 443 端口),否则证书申请会失败。

双向 TLS(mTLS)

普通 HTTPS 只验证服务器身份,双向 TLS 还要求客户端提供证书:

package main

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
    "net/http"
    
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    
    router.GET("/", func(c *gin.Context) {
        c.String(200, "Hello mTLS")
    })

    // 读取 CA 证书
    caCert, err := ioutil.ReadFile("ca.crt")
    if err != nil {
        log.Fatal(err)
    }
    
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    // TLS 配置
    cfg := &tls.Config{
        ClientCAs:  caCertPool,
        ClientAuth: tls.RequireAndVerifyClientCert,
    }

    srv := &http.Server{
        Addr:      ":443",
        Handler:   router,
        TLSConfig: cfg,
    }

    log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))
}

客户端访问时需要提供证书:

curl --cert client.crt --key client.key https://example.com

mTLS 常用于内部服务之间的认证,比用户名密码更安全。

TLS 配置最佳实践

1. 禁用旧版本协议

cfg := &tls.Config{
    MinVersion: tls.VersionTLS12, // 最低 TLS 1.2
}

TLS 1.0 和 1.1 已经被认为不安全,应该禁用。

2. 选择安全的加密套件

cfg := &tls.Config{
    CipherSuites: []uint16{
        tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
        tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
        tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
    },
}

避免使用 RC4、DES、CBC 模式等不安全的加密算法。

3. 启用 HSTS

通过响应头告诉浏览器始终使用 HTTPS:

router.Use(func(c *gin.Context) {
    c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
    c.Next()
})

4. 使用强 DH 参数

如果使用 DH 密钥交换,应该使用 2048 位以上的参数:

openssl dhparam -out dhparam.pem 2048
// Go 1.8+ 默认使用安全的 DH 参数,通常不需要额外配置

测试 HTTPS 配置

可以用 openssl 测试服务器的 TLS 配置:

# 测试连接
openssl s_client -connect example.com:443

# 测试证书
openssl s_client -connect example.com:443 -showcerts

# 测试支持的协议和加密套件
nmap --script ssl-enum-ciphers -p 443 example.com

也可以用在线工具如 SSL Labs(https://www.ssllabs.com/ssltest/)测试。

小结

Gin 启用 HTTPS 很简单,关键是:

  • 开发环境用自签名证书,生产环境用正规 CA 签发的证书
  • 配置安全的 TLS 参数(协议版本、加密套件)
  • HTTP 自动跳转 HTTPS
  • 考虑用 autocert 自动管理 Let's Encrypt 证书
  • 内部服务可以考虑用 mTLS 增强安全性

HTTPS 不仅能保护用户数据,还能提升网站可信度和搜索排名,是现代 Web 服务的必备配置。