在上一篇文章中,我们使用 go:embed
功能,成功将静态资源文件嵌入到可执行程序中,这次我们尝试将 https 证书嵌入到 程序中,并实现 Gin+HTTPS 功能。
为什么需要 HTTPS?
首先我们需要了解一下,为什么需要在程序中嵌入一个 HTTPS 服务。
在上一篇笔记的开头我们提到,通过嵌入资源文件可以让最终软件在分发、部署时更简单,那我们服务本身就需要提供一个 Web 页面供用户进行操作。既然是 Web 页面,就一定会涉及到浏览器与服务端数据交互的场景。如果服务需要通过非本地 IP(既,不是 127.0.0.1 需要通过网络设备远程访问,包括但不限于 局域网、公网等)来访问,那么我建议你一定要通过 HTTPS 协议进行数据交互。
三点原因如下
- 通过 HTTPS 协议可以加密传输数据,防止中间人攻击,保障用户数据安全。
- 在企业内网或高校校园网等内部网络中,为确保安全,通常会使用一些网络防火墙产品。使用 HTTP 协议可能导致两个问题:一是防火墙记录明文请求数据,导致敏感数据泄露;二是防火墙解析服务器返回的数据后拦截响应,导致服务异常。
- 浏览器厂商越来越重视安全问题,目前很多前端的 API 函数 只允许在 HTTPS 协议中调用
如果使用 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)
...
效果展示
不可信的证书,浏览器会提示不信任,点击高级 继续前往 即可正常访问。
启动 HTTPS 协议,页面访问、资源加载 都是 HTTPS