提示:本节课最终代码为:feature/s20

在腾讯很多应用都会强制要求通过 HTTPS 协议进行通信,Kubernetes 也提倡使用 HTTPS 协议通信。那么为什么都建议通过 HTTPS 协议而非 HTTP 协议进行通信呢?这要从二者的不同说起。

  • HTTP 协议以明文方式传输数据,在网络上进行传输可能会被窃听、篡改甚至冒充,安全性较低,数据泄露风险大;
  • HTTPS 基于 HTTP 协议加入了 SSL 安全层,通过加密的通道进行数据传输,安全性更大。

通常来说,HTTPS 协议主要实现以下 2 类功能:

  1. 数据传输加密:通过 HTTPS 传输的数据始终会被加密,因此,信息高度安全;
  2. 身份认证:HTTPS 支持单向认证和双向认证,单向认证可以用来确认服务端的真实性。双向认证既可以验证服务端的真实性,又可以验证客户端的合法性。

在真实的企业应用中,我们通常可以通过以下 2 种方式来使用 HTTPS:

  1. 使用 HTTPS 的数据传输加密能力,开启 HTTPS 单向认证,验证服务端合法性。服务端对客户端的认证则采用其他认证方式,例如:Bearer 认证。miniblog 就使用了这种方式;
  2. 使用 HTTPS 的数据传输加密能力,并通过 HTTPS 进行双向认证。

因为 CA 认证、HTTPS 认证流程相关内容,在面试中经常会被问到,并且 miniblog 开启 HTTPS 服务,也需要你理解 CA 认证、HTTPS 认证流程。所以,接下来,我会详细介绍这两部分内容。

认识 CA 证书

CA 证书内容很多,你可以通过以下 4 个方面来掌握 CA 证书相关内容:

  1. CA 证书相关的名词;
  2. CA 证书的签发流程;
  3. CA 证书的认证流程;
  4. CA 证书签发实战。

CA 证书相关名词介绍

CA 证书有很多相关的名词,这里整理一些核心的名词,供你参考。

  • CA:数字证书认证机构(Certicate Authority,缩写为 CA),是负责发放和管理数字证书的权威机构,并作为受信任的第三方,承担公钥体系中公钥合法性检验的责任;

  • CA 证书:CA 证书即由 CA 签发的数字证书。数字证书可以有不同的格式,当前用的最多的是 X.509 证书格式;

  • 公钥和私钥:公钥(Public Key)与私钥(Private Key)是通过一种算法得到的一个密钥对,公钥是密钥对中公开的部分,私钥则是非公开的部分。公钥通常用于加密会话密钥、验证数字签名,或加密可以用相应的私钥解密的数据。

  • 可以使用不同的加密算法加密数据,包括以下两种加密算法:

    • 对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有 DES、AES 等。
    • 非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有 RSA、DSA 等。
  • X.509 证书,可能有不同的编码格式,目前有以下两种编码格式:

    • PEM(Privacy Enhanced Mail):打开看文本格式,以 -----BEGIN XXXXXX----- 开头,-----END XXXXXX----- 结尾,内容是 BASE64 编码。Apache 和 UNIX 服务器偏向于使用这种编码格式;
    • DER(Distinguished Encoding Rules):打开看是二进制格式,不可读。Java 和 Windows 服务器偏向于使用这种编码格式。
  • X.509 证书,有 PEM 和 DER 2 种编码模式,但其文件后缀并不一定是 .pem.der,可能是以下几种:

    • CRT(.crt):CRT 代表 certificate,是证书的意思。常见于 UNIX 系统,有可能是 PEM 编码,也有可能是 DER 编码,大多数是 PEM 编码。
    • CER(.cer):CER 代表 certificate,是证书的意思。常见于 Windows 系统,同样的可能是 PEM 编码,也可能是 DER 编码,大多数应该是 DER 编码;
    • KEY(.key):通常用来存放一个公钥或者私钥,并非 X.509 证书。编码格式可能是 PEM,也可能是 DER。
    • CSR(.csr):Certificate Signing Request,即证书签名请求。这个并不是证书,而是向权威证书颁发机构获得签名证书的申请,其核心内容是一个公钥(还附带了一些别的信息)。在生成这个申请的时候,同时也会生成一个私钥,私钥需要自己保管好,不用提供给 CA 机构。

这里需要注意,如果证书文件后缀是 .pem.der,需要在文件名中展示出文件的功能类别,例如:server-key.pem(私钥文件)、server-crt.pem(证书文件)。

为了能够让你更好理解 CA,我会以 CA 签发流程为主线,在介绍签发流程的过程中,介绍相关的概念。CA 的认证流程,其实也就是 HTTPS 的认证流程,会放在下一小节详细介绍。

CA 证书的签发流程

CA 证书认证流程如下图(网图)所示:

img

  1. 服务端 S 向第三方机构 CA 提交公钥、组织信息、个人信息(域名)等信息并申请认证(不需要提交私钥)。

  2. CA 通过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等。

  3. 如信息审核通过,CA 会向申请者签发认证文件(证书):

    1. 证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA 的信息、有效时间、证书序列号等信息的明文,同时包含一个签名;
    2. 签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA 的私钥对信息摘要进行加密,密文即签名。
  4. 客户端 C 向服务器 S 发出请求时,S 返回服务端的证书文件。

  5. 客户端 C 读取证书中相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应 CA 的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即公钥合法;客户端然后验证证书相关的域名信息、有效时间等信息。客户端会内置信任 CA 的证书(根证书)信息(包含公钥),如果 CA 不被信任,则找不到对应 CA 的证书,证书也会被判定非法。

  6. 客户端会生成一个随机数 R,并用服务端证书的公钥加密 R,返回给服务端。服务端使用自己的私钥解密后获取随机数 R,之后就采用对称加密算法进行数据交换。

提示:证书 = 公钥(服务端生成密码对中的公钥)+ 申请者与颁发者信息 + 签名(用CA机构生成的密码对的私钥进行签名)。

颁发证书其实就是使用 CA 的私钥对证书请求签名文件进行签名。

CA 认证流程

CA 证书的认证分为单向认证和双向认证,在实际开发过程中,可根据需要自行选择:

  • 单向认证:用户数目广泛,且无需在通讯层对用户身份进行验证,一般都是在应用逻辑层保证用户的合法登入;
  • 双向认证:要求通信双方要相互验证身份。例如,在企业的应用服务之间存在调用关系的时候,可能需要对通信双方做身份验证。

单向认证流程

单向认证流程如下图所示:

img

整个流程图中已经说的很清晰了,这里不再赘述。

需要注意:第 3 步用的是 CA 机构证书(根证书)的公钥解密签名。

通过上述单向认证流程,可以知道整个单向认证的流程需要 3 个证书文件:

  • 根证书;
  • 服务器端公钥证书;
  • 服务器端私钥文件。

在上面这个过程中,我们可以看到单向认证仅仅验证了服务端的身份,如果有人冒充了客户端,那该怎么办?我们可以采用双向认证。

双向认证流程

双向认证 SSL 握手过程与单向认证有所不同。大部分步骤与单向认证过程一样。但是,当客户端成功验证服务器后,会增加服务器验证客户端的流程步骤,具体如下图红色字体标注所示:

img

通过上述双向认证流程,可以知道整个双向认证的流程需要 6 个证书文件:

  • 根证书;
  • 服务器端公钥证书;
  • 服务器端私钥文件;
  • 客户端公钥证书;
  • 客户端私钥文件。

CA 证书签发实战

签发 CA 证书是由权威的 CA 机构签发的,签发流程麻烦,并且收费贵。在后端应用开发中,通常由开发/运维自己生成根证书和根证书私钥,然后扮演 CA 的角色,负责给其他服务端和客户端签发证书(这些签发的证书也叫自签证书)。

接下来,我会使用 openssl 工具,一步一步给你展示如何签发根证书、服务端证书、客户端证书。openssl 命令使用方式可参考:Openssl子命令 genrsa, rsa, req, x509命令详解

步骤一:签发根证书和私钥。

  1. 生成根证书私钥。
  1. $ openssl genrsa -out ca.key 1024

genrsa 子命令主要用于生成 RSA 私钥。命令行格式:openssl genrsa [args] [numbits]。涉及参数说明如下:

  • -out file: 将生成的私钥保存至指定的文件中;
  • numbits:指定生成私钥的大小,默认是 2048。
  1. 生成请求文件。
  1. $ openssl req -new -key ca.key -out ca.csr -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=devops/OU=it/CN=127.0.0.1/emailAddress=nosbelm@qq.com"

req 子命令主要用于创建证书请求文件。命令行格式:openssl req [options] <infile> outfile。涉及参数说明如下:

  • -new:创建新的证书请求文件;
  • -key file:指定创建证书请求的私钥文件;
  • -out arg:指定输出文件;
  • -subj arg:设置或修改证书请求的主体信息。

证书请求的主体信息选项说明如下:

  • CN: Common Name,该项指定了证书中标识你身份的名字,该名字具有唯一性。一般来讲就是填写你将要申请SSL证书的域名 (domain)或子域名(sub domain)。
  • C:country;
  • ST:state;
  • L:city;
  • O: Organization,证书所属组;
  • OU:organization unit。

注意:

  1. 不同证书 csr 文件的 CNCSTLOOU 组合必须不同,否则可能出现

PEER'S CERTIFICATE HAS AN INVALID SIGNATURE 错误;

  1. 后续创建证书的 csr 文件时,CN 都不相同(CSTLOOU相同),以达到

区分的目的;

  1. 生成根证书。
  1. $ openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt

x509 命令主要用于创建、修改 x509 证书。命令行格式:openssl x509 [options] <infile >outfile。涉及参数说明如下:

  • -in arg:指定输入文件,默认为标准输入;
  • -out arg:指定输出文件,默认为标准输出;
  • -req:输入文件为证书请求;
  • -signkey arg:指定自签名私钥文件;
  • -CA arg:设置 CA 文件,必须为 PEM 格式;
  • -CAkey arg:设置 CA 私钥文件,必须为 PEM 格式;
  • -CAcreateserial:创建序列号文件。

步骤二:生成服务端证书。

  1. 生成服务端私钥。
  1. $ openssl genrsa -out server.key 1024
  1. 生成服务端公钥。
  1. $ openssl rsa -in server.key -pubout -out server.pem

rsa 子命令主要用于处理 RSA 公私钥文件。命令行格式:openssl rsa [options] <infile> outfile。涉及参数说明如下:

  • -in arg: 指定输入文件;
  • -pubout:指定输出文件为公钥,默认为私钥;
  • -out arg:指定输出文件。
  1. 生成服务端向 CA 申请签名的 CSR。
  1. $ openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=serverdevops/OU=serverit/CN=127.0.0.1/emailAddress=nosbelm@qq.com"
  1. 生成服务端带有 CA 签名的证书。
  1. $ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt

步骤三:生成客户端证书。

  1. 生成客户端私钥。
  1. $ openssl genrsa -out client.key 1024
  1. 生成客户端公钥。
  1. $ openssl rsa -in client.key -pubout -out client.pem
  1. 生成客户端向 CA 申请签名的 CSR。
  1. $ openssl req -new -key client.key -out client.csr -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=clientdevops/OU=clientit/CN=127.0.0.1/emailAddress=nosbelm@qq.com"
  1. 生成客户端带有 CA 签名的证书。
  1. $ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in client.csr -out client.crt

miniblog 添加 HTTPS 支持

通过上面的学习,我们知道 miniblog 开启 HTTPS 的方式为:使用 HTTPS 的数据传输加密能力,开启 HTTPS 单向认证,验证服务端合法性。服务端对客户端的认证则采用 JWT Token 认证。功能实现流程分以下 3 步:

  1. 生成根证书、服务端证书、服务端私钥;
  2. 配置 tls 证书文件和 HTTPS 服务监听端口;
  3. 读取 tls 证书文件,并启动 HTTPS 服务器。

步骤一:生成根证书、服务端证书、服务端私钥。

Makefile 文件中添加以下目标,用来自动生成需要的 CA 证书文件:

  1. .PHONY: ca
  2. ca: ## 生成 CA 文件
  3. @mkdir -p $(OUTPUT_DIR)/cert
  4. @openssl genrsa -out $(OUTPUT_DIR)/cert/ca.key 1024 # 生成根证书私钥
  5. @openssl req -new -key $(OUTPUT_DIR)/cert/ca.key -out $(OUTPUT_DIR)/cert/ca.csr \
  6. -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=devops/OU=it/CN=127.0.0.1/emailAddress=nosbelm@qq.com" # 2. 生成请求文件
  7. @openssl x509 -req -in $(OUTPUT_DIR)/cert/ca.csr -signkey $(OUTPUT_DIR)/cert/ca.key -out $(OUTPUT_DIR)/cert/ca.crt # 3. 生成根证书
  8. @openssl genrsa -out $(OUTPUT_DIR)/cert/server.key 1024 # 4. 生成服务端私钥
  9. @openssl rsa -in $(OUTPUT_DIR)/cert/server.key -pubout -out $(OUTPUT_DIR)/cert/server.pem # 5. 生成服务端公钥
  10. @openssl req -new -key $(OUTPUT_DIR)/cert/server.key -out $(OUTPUT_DIR)/cert/server.csr \
  11. -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=serverdevops/OU=serverit/CN=127.0.0.1/emailAddress=nosbelm@qq.com" # 6. 生成服务端向 CA 申请签名的 CSR
  12. @openssl x509 -req -CA $(OUTPUT_DIR)/cert/ca.crt -CAkey $(OUTPUT_DIR)/cert/ca.key \
  13. -CAcreateserial -in $(OUTPUT_DIR)/cert/server.csr -out $(OUTPUT_DIR)/cert/server.crt # 7. 生成服务端带有 CA 签名的证书

步骤二:配置 tls 证书文件和 HTTPS 服务监听端口。

configs/miniblog.yaml 文件中,添加以下配置:

  1. # HTTPS 服务器相关配置
  2. tls:
  3. addr: :8443 # HTTPS 服务器监听地址
  4. cert: ./_output/cert/server.crt # 服务端证书文件
  5. key: ./_output/cert/server.key # 服务端私钥文件

步骤三:读取 tls 证书文件,并启动 HTTPS 服务器。

在文件 internal/miniblog/miniblog.go 中通过以下代码启动 HTTPS 服务:

  1. // startSecureServer 创建并运行 HTTPS 服务器.
  2. func startSecureServer(g *gin.Engine) *http.Server {
  3. // 创建 HTTPS Server 实例
  4. httpssrv := &http.Server{Addr: viper.GetString("tls.addr"), Handler: g}
  5. // 运行 HTTPS 服务器。在 goroutine 中启动服务器,它不会阻止下面的正常关闭处理流程
  6. // 打印一条日志,用来提示 HTTPS 服务已经起来,方便排障
  7. log.Infow("Start to listening the incoming requests on https address", "addr", viper.GetString("tls.addr"))
  8. cert, key := viper.GetString("tls.cert"), viper.GetString("tls.key")
  9. if cert != "" && key != "" {
  10. go func() {
  11. if err := httpssrv.ListenAndServeTLS(cert, key); err != nil && !errors.Is(err, http.ErrServerClosed) {
  12. log.Fatalw(err.Error())
  13. }
  14. }()
  15. }
  16. return httpssrv
  17. }

可以看到,我们使用了 Go 语言 net/http 包中的 ListenAndServeTLS() 方法来启动一个 HTTPS 服务。该方法需要:服务端证书文件和服务端私钥文件 2 个参数。

这里,还需要记得添加 HTTPS 服务的优雅关停代码:

  1. // 等待中断信号优雅地关闭服务器(10 秒超时)。
  2. //quit := make(chan os.Signal, 1)
  3. quit := make(chan os.Signal)
  4. ......
  5. if err := httpssrv.Shutdown(ctx); err != nil {
  6. log.Errorw("Secure Server forced to shutdown", "err", err)
  7. return err
  8. }
  9. ......

为了使代码看起来更加简洁,这里我重构了 internal/miniblog/miniblog.go 文件中的代码,将启动 HTTP 服务的代码和启动 HTTPS 服务的代码分别放在了 startInsecureServerstartSecureServer 函数中,以做到函数级别的隔离,并且使主流程看起来更清晰。

开发过程中,不断重构、优化你的代码是一个很正常,并且非常有意义的工作。

编译并测试

  1. $ make
  2. $ make ca # 生成根证书、服务端证书、服务端私钥
  3. $ _output/miniblog -c configs/miniblog.yaml

新打开一个 Linux 终端。并执行以下操作:

  1. 创建一个测试用户。
  1. curl -XPOST -H"Content-Type: application/json" -d'{"username":"catest","password":"miniblog1234","nickname":"catest","email":"catest@qq.com","phone":"18188888xxx"}' http://127.0.0.1:8080/v1/users
  1. 登录测试用户。
  1. token=`curl -s -XPOST -H"Content-Type: application/json" -d'{"username":"catest","password":"miniblog1234"}' http://127.0.0.1:8080/login | jq -r .token`
  1. 获取用户详细信息。
  1. # 1. 通过 HTTPS 协议访问 miniblog。不指定根证书,无法认证服务端证书报错
  2. $ curl -XGET -H"Authorization: Bearer $token" https://127.0.0.1:8443/v1/users/catest
  3. curl: (60) SSL certificate problem: unable to get local issuer certificate
  4. More details here: https://curl.haxx.se/docs/sslcerts.html
  5. curl failed to verify the legitimacy of the server and therefore could not
  6. establish a secure connection to it. To learn more about this situation and
  7. how to fix it, please visit the web page mentioned above.
  8. # 2. 读取根证书,并使用根证书认证服务端
  9. $ curl -XGET --cacert _output/cert/ca.crt -H"Authorization: Bearer $token" https://127.0.0.1:8443/v1/users/catest
  10. {"username":"catest","nickname":"catest","email":"catest@qq.com","phone":"18188888xxx","postCount":0,"createdAt":"2022-12-08 15:43:10","updatedAt":"2022-12-08 15:43:10"}
  11. # 3. 忽略 HTTPS 证书参数,指定跳过 SSL 检测
  12. $ curl -XGET -k -H"Authorization: Bearer $token" https://127.0.0.1:8443/v1/users/catest
  13. {"username":"catest","nickname":"catest","email":"catest@qq.com","phone":"18188888xxx","postCount":0,"createdAt":"2022-12-08 15:43:10","updatedAt":"2022-12-08 15:43:10"}

小结

使用 HTTPS 协议进行通信,是企业级应用最基本的安全保障手段。本节课详细介绍了 CA 证书,以及如何实现 HTTPS 通信。