MENU

go:embed 嵌入 HTTPS 证书

June 30, 2024 • Read: 143 • 编码,Go

在上一篇文章中,我们使用 go:embed 功能,成功将静态资源文件嵌入到可执行程序中,这次我们尝试将 https 证书嵌入到 程序中,并实现 Gin+HTTPS 功能。

为什么需要 HTTPS?

首先我们需要了解一下,为什么需要在程序中嵌入一个 HTTPS 服务。

在上一篇笔记的开头我们提到,通过嵌入资源文件可以让最终软件在分发、部署时更简单,那我们服务本身就需要提供一个 Web 页面供用户进行操作。既然是 Web 页面,就一定会涉及到浏览器与服务端数据交互的场景。如果服务需要通过非本地 IP(既,不是 127.0.0.1 需要通过网络设备远程访问,包括但不限于 局域网、公网等)来访问,那么我建议你一定要通过 HTTPS 协议进行数据交互。

两点原因如下

  1. 通过 HTTPS 协议可以加密传输数据,防止中间人攻击,保障用户数据安全。
  2. 在企业内网或高校校园网等内部网络中,为确保安全,通常会使用一些网络防火墙产品。使用 HTTP 协议可能导致两个问题:一是防火墙记录明文请求数据,导致敏感数据泄露;二是防火墙解析服务器返回的数据后拦截响应,导致服务异常。

如果使用 HTTPS 协议可以避免这些问题,就可以尽可能避免这些问题。

内网服务也需要 HTTPS 吗

一些开发者可能认为:“我的服务只部署在内部网络中,不对公众网络开放,即使不使用 HTTPS 协议,安全性也不会有问题。”

对此我的观点是:“作为开发者,我们不能依赖环境来保障项目的安全性。内网环境并非绝对安全,我们应该尽可能提高项目自身的安全级别。所有的安全防御手段只有在攻击发生前实施才有效,一旦发生数据泄露,后续的补救措施也无法弥补损失。”

内网 HTTPS 证书无效问题

如果内网没有域名、即使使用证书,浏览器也会标记证书无效,那么在内网使用 HTTPS 证书还有意义吗?

在内网环境中,由于没有没有域名,我们的 HTTPS 证书有两个来源,第一种是使用 CA 机构颁发的授信证书,但这种证书只会颁发给域名,给内网 IP 使用时会提示证书无效。第二种,是自签名证书,可以签给ip地址的,但自签名的证书不会被客户端浏览器信任,所以会也会提示证书无效。

即使内网环境中证书不被信任,我们仍应该使用 HTTPS 证书,不被信任的证书仍可以用于通信数据的加密、数据完整性的校验,保障数据不被中间人获取、篡改。

例如 1Panel Web 管理面板就支持开启 "面板 SSL" 功能,虽然开启后自签名的证书不被信任,但仍可以通过 HTTPS 提升访问的安全性。

自签名证书

获取证书的方式有两种,一种是可信 CA 机构签发的域名证书,这种不做赘述,这次我们用自签名证书。

要实现自签名证书需要电脑安装 openssl 这个软件,如果是 Linux 系统,绝大部分发行版都会带。如果是 Windows 系统,如果有安装 Git 则可以直接在 Git Bash 终端中直接使用 openssl

首先在 终端中进入项目目录,在需要存放证书的目录执行命令生成证书,

第一步 先生成私钥(.key 文件)

openssl genpkey -algorithm RSA -out server.key -pkeyopt rsa_keygen_bits:2048

第二步 生成证书(.ctr 或 pem) 文件,-days 表示证书自签发之日起有效天数,我一般写 3650 天(10 年),因为证书嵌入到二进制文件中分发给用户,不能每年换证书文件。

openssl req -new -x509 -sha256 -key server.key -out server.crt -days 365

(可选)如果需要 PEM 格式的证书文件,需要执行如下命令合并

cat server.key server.crt > server.pem

嵌入证书,Gin 实现 HTTPS

首先,通过 go:embed 在编译时,将证书加载到 SSLCertFile、SSLCertFile 变量中,然后通过 自定义 HTTP 配置的方式加载证书,不用引入任何多余的包。

ackage main

import (
    "crypto/tls"
    "embed"
    "github.com/gin-gonic/gin"
    "io/fs"
    "net/http"
)

var (
    //go:embed common/cert/server.crt
    SSLCertFile []byte
    //go:embed common/cert/server.key
    SSLKeyFile []byte
    //go:embed web/*
    WebDir embed.FS
    App    *gin.Engine
)

func main() {
    App = gin.New()
    // 这是一个接口
    App.GET("/api", func(context *gin.Context) {
        context.JSONP(http.StatusOK, gin.H{
            "code":    0,
            "message": "hi,dbkuaizi",
        })
    })
    // 嵌入的 embed 就是一个文件系统,获取 web 目录下的资源文件
    staticFp, _ := fs.Sub(WebDir, "web")
    // 所有请求先匹配 api 路由,如果没匹配到就当作静态资源文件处理
    App.NoRoute(gin.WrapH(http.FileServer(http.FS(staticFp))))
    // http 协议的服务
    //App.Run(":2333")

    // 不能这样用!!! 因为 RunTLS 底层是把 第二个、第三个参数当作一个路径去加载
    //App.RunTLS(":2333", string(SSLCertFile), string(SSLKeyFile))

    // 载入 embed 中的证书文件,返回证书连
    cert, _ := tls.X509KeyPair(SSLCertFile, SSLKeyFile)
    // 因为 Gin 不支持从 embed 中载入证书,所以需要自定义证书
    Server := &http.Server{
        Addr:    ":2333",
        Handler: App, // 加载 Gin 路由
        TLSConfig: &tls.Config{
            // 加载自签名证书
            Certificates: []tls.Certificate{cert},
        },
    }
    // HTTPS,启动!
    Server.ListenAndServeTLS("", "")
}

PEM 证书

PEM 格式的证书包含了 私钥和证书两部分,所以只需用嵌入一次即可:

var (
    //go:embed common/cert/server.pem
    SSLPemFile []byte
    //go:embed web/*
    WebDir embed.FS
    App    *gin.Engine
)

...

// 载入 embed 中的证书文件,返回证书连
    cert, _ := tls.X509KeyPair(SSLPemFile, SSLPemFile)
...

效果展示

不可信的证书,浏览器会提示不信任,点击高级 继续前往 即可正常访问。
Snipaste_2024-06-30_23-27-10.webp

启动 HTTPS 协议,页面访问、资源加载 都是 HTTPS
Snipaste_2024-06-30_23-23-14.webp