提示:本节课最终代码为:feature/s20。
在腾讯很多应用都会强制要求通过 HTTPS 协议进行通信,Kubernetes 也提倡使用 HTTPS 协议通信。那么为什么都建议通过 HTTPS 协议而非 HTTP 协议进行通信呢?这要从二者的不同说起。
- HTTP 协议以明文方式传输数据,在网络上进行传输可能会被窃听、篡改甚至冒充,安全性较低,数据泄露风险大;
- HTTPS 基于 HTTP 协议加入了 SSL 安全层,通过加密的通道进行数据传输,安全性更大。
通常来说,HTTPS 协议主要实现以下 2 类功能:
- 数据传输加密:通过 HTTPS 传输的数据始终会被加密,因此,信息高度安全;
- 身份认证:HTTPS 支持单向认证和双向认证,单向认证可以用来确认服务端的真实性。双向认证既可以验证服务端的真实性,又可以验证客户端的合法性。
在真实的企业应用中,我们通常可以通过以下 2 种方式来使用 HTTPS:
- 使用 HTTPS 的数据传输加密能力,开启 HTTPS 单向认证,验证服务端合法性。服务端对客户端的认证则采用其他认证方式,例如:Bearer 认证。miniblog 就使用了这种方式;
- 使用 HTTPS 的数据传输加密能力,并通过 HTTPS 进行双向认证。
因为 CA 认证、HTTPS 认证流程相关内容,在面试中经常会被问到,并且 miniblog 开启 HTTPS 服务,也需要你理解 CA 认证、HTTPS 认证流程。所以,接下来,我会详细介绍这两部分内容。
认识 CA 证书
CA 证书内容很多,你可以通过以下 4 个方面来掌握 CA 证书相关内容:
- CA 证书相关的名词;
- CA 证书的签发流程;
- CA 证书的认证流程;
- 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 服务器偏向于使用这种编码格式。
- PEM(Privacy Enhanced Mail):打开看文本格式,以
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 机构。
- CRT(
这里需要注意,如果证书文件后缀是 .pem
或 .der
,需要在文件名中展示出文件的功能类别,例如:server-key.pem
(私钥文件)、server-crt.pem
(证书文件)。
为了能够让你更好理解 CA,我会以 CA 签发流程为主线,在介绍签发流程的过程中,介绍相关的概念。CA 的认证流程,其实也就是 HTTPS 的认证流程,会放在下一小节详细介绍。
CA 证书的签发流程
CA 证书认证流程如下图(网图)所示:
服务端 S 向第三方机构 CA 提交公钥、组织信息、个人信息(域名)等信息并申请认证(不需要提交私钥)。
CA 通过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等。
如信息审核通过,CA 会向申请者签发认证文件(证书):
- 证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA 的信息、有效时间、证书序列号等信息的明文,同时包含一个签名;
- 签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA 的私钥对信息摘要进行加密,密文即签名。
客户端 C 向服务器 S 发出请求时,S 返回服务端的证书文件。
客户端 C 读取证书中相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应 CA 的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即公钥合法;客户端然后验证证书相关的域名信息、有效时间等信息。客户端会内置信任 CA 的证书(根证书)信息(包含公钥),如果 CA 不被信任,则找不到对应 CA 的证书,证书也会被判定非法。
客户端会生成一个随机数 R,并用服务端证书的公钥加密 R,返回给服务端。服务端使用自己的私钥解密后获取随机数 R,之后就采用对称加密算法进行数据交换。
提示:证书 = 公钥(服务端生成密码对中的公钥)+ 申请者与颁发者信息 + 签名(用CA机构生成的密码对的私钥进行签名)。
颁发证书其实就是使用 CA 的私钥对证书请求签名文件进行签名。
CA 认证流程
CA 证书的认证分为单向认证和双向认证,在实际开发过程中,可根据需要自行选择:
- 单向认证:用户数目广泛,且无需在通讯层对用户身份进行验证,一般都是在应用逻辑层保证用户的合法登入;
- 双向认证:要求通信双方要相互验证身份。例如,在企业的应用服务之间存在调用关系的时候,可能需要对通信双方做身份验证。
单向认证流程
单向认证流程如下图所示:
整个流程图中已经说的很清晰了,这里不再赘述。
需要注意:第 3 步用的是 CA 机构证书(根证书)的公钥解密签名。
通过上述单向认证流程,可以知道整个单向认证的流程需要 3 个证书文件:
- 根证书;
- 服务器端公钥证书;
- 服务器端私钥文件。
在上面这个过程中,我们可以看到单向认证仅仅验证了服务端的身份,如果有人冒充了客户端,那该怎么办?我们可以采用双向认证。
双向认证流程
双向认证 SSL 握手过程与单向认证有所不同。大部分步骤与单向认证过程一样。但是,当客户端成功验证服务器后,会增加服务器验证客户端的流程步骤,具体如下图红色字体标注所示:
通过上述双向认证流程,可以知道整个双向认证的流程需要 6 个证书文件:
- 根证书;
- 服务器端公钥证书;
- 服务器端私钥文件;
- 客户端公钥证书;
- 客户端私钥文件。
CA 证书签发实战
签发 CA 证书是由权威的 CA 机构签发的,签发流程麻烦,并且收费贵。在后端应用开发中,通常由开发/运维自己生成根证书和根证书私钥,然后扮演 CA 的角色,负责给其他服务端和客户端签发证书(这些签发的证书也叫自签证书)。
接下来,我会使用 openssl
工具,一步一步给你展示如何签发根证书、服务端证书、客户端证书。openssl
命令使用方式可参考:Openssl子命令 genrsa, rsa, req, x509命令详解。
步骤一:签发根证书和私钥。
- 生成根证书私钥。
$ openssl genrsa -out ca.key 1024
genrsa
子命令主要用于生成 RSA 私钥。命令行格式:openssl genrsa [args] [numbits]
。涉及参数说明如下:
-out file
: 将生成的私钥保存至指定的文件中;numbits
:指定生成私钥的大小,默认是 2048。
- 生成请求文件。
$ 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。
注意:
- 不同证书 csr 文件的
CN
、C
、ST
、L
、O
、OU
组合必须不同,否则可能出现
PEER'S CERTIFICATE HAS AN INVALID SIGNATURE
错误;
- 后续创建证书的 csr 文件时,
CN
都不相同(C
、ST
、L
、O
、OU
相同),以达到
区分的目的;
- 生成根证书。
$ 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
:创建序列号文件。
步骤二:生成服务端证书。
- 生成服务端私钥。
$ openssl genrsa -out server.key 1024
- 生成服务端公钥。
$ openssl rsa -in server.key -pubout -out server.pem
rsa
子命令主要用于处理 RSA 公私钥文件。命令行格式:openssl rsa [options] <infile> outfile
。涉及参数说明如下:
-in arg
: 指定输入文件;-pubout
:指定输出文件为公钥,默认为私钥;-out arg
:指定输出文件。
- 生成服务端向 CA 申请签名的 CSR。
$ 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"
- 生成服务端带有 CA 签名的证书。
$ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt
步骤三:生成客户端证书。
- 生成客户端私钥。
$ openssl genrsa -out client.key 1024
- 生成客户端公钥。
$ openssl rsa -in client.key -pubout -out client.pem
- 生成客户端向 CA 申请签名的 CSR。
$ 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"
- 生成客户端带有 CA 签名的证书。
$ 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 步:
- 生成根证书、服务端证书、服务端私钥;
- 配置 tls 证书文件和 HTTPS 服务监听端口;
- 读取 tls 证书文件,并启动 HTTPS 服务器。
步骤一:生成根证书、服务端证书、服务端私钥。
在 Makefile
文件中添加以下目标,用来自动生成需要的 CA 证书文件:
.PHONY: ca
ca: ## 生成 CA 文件
@mkdir -p $(OUTPUT_DIR)/cert
@openssl genrsa -out $(OUTPUT_DIR)/cert/ca.key 1024 # 生成根证书私钥
@openssl req -new -key $(OUTPUT_DIR)/cert/ca.key -out $(OUTPUT_DIR)/cert/ca.csr \
-subj "/C=CN/ST=Guangdong/L=Shenzhen/O=devops/OU=it/CN=127.0.0.1/emailAddress=nosbelm@qq.com" # 2. 生成请求文件
@openssl x509 -req -in $(OUTPUT_DIR)/cert/ca.csr -signkey $(OUTPUT_DIR)/cert/ca.key -out $(OUTPUT_DIR)/cert/ca.crt # 3. 生成根证书
@openssl genrsa -out $(OUTPUT_DIR)/cert/server.key 1024 # 4. 生成服务端私钥
@openssl rsa -in $(OUTPUT_DIR)/cert/server.key -pubout -out $(OUTPUT_DIR)/cert/server.pem # 5. 生成服务端公钥
@openssl req -new -key $(OUTPUT_DIR)/cert/server.key -out $(OUTPUT_DIR)/cert/server.csr \
-subj "/C=CN/ST=Guangdong/L=Shenzhen/O=serverdevops/OU=serverit/CN=127.0.0.1/emailAddress=nosbelm@qq.com" # 6. 生成服务端向 CA 申请签名的 CSR
@openssl x509 -req -CA $(OUTPUT_DIR)/cert/ca.crt -CAkey $(OUTPUT_DIR)/cert/ca.key \
-CAcreateserial -in $(OUTPUT_DIR)/cert/server.csr -out $(OUTPUT_DIR)/cert/server.crt # 7. 生成服务端带有 CA 签名的证书
步骤二:配置 tls 证书文件和 HTTPS 服务监听端口。
在 configs/miniblog.yaml
文件中,添加以下配置:
# HTTPS 服务器相关配置
tls:
addr: :8443 # HTTPS 服务器监听地址
cert: ./_output/cert/server.crt # 服务端证书文件
key: ./_output/cert/server.key # 服务端私钥文件
步骤三:读取 tls 证书文件,并启动 HTTPS 服务器。
在文件 internal/miniblog/miniblog.go
中通过以下代码启动 HTTPS 服务:
// startSecureServer 创建并运行 HTTPS 服务器.
func startSecureServer(g *gin.Engine) *http.Server {
// 创建 HTTPS Server 实例
httpssrv := &http.Server{Addr: viper.GetString("tls.addr"), Handler: g}
// 运行 HTTPS 服务器。在 goroutine 中启动服务器,它不会阻止下面的正常关闭处理流程
// 打印一条日志,用来提示 HTTPS 服务已经起来,方便排障
log.Infow("Start to listening the incoming requests on https address", "addr", viper.GetString("tls.addr"))
cert, key := viper.GetString("tls.cert"), viper.GetString("tls.key")
if cert != "" && key != "" {
go func() {
if err := httpssrv.ListenAndServeTLS(cert, key); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalw(err.Error())
}
}()
}
return httpssrv
}
可以看到,我们使用了 Go 语言 net/http
包中的 ListenAndServeTLS()
方法来启动一个 HTTPS 服务。该方法需要:服务端证书文件和服务端私钥文件 2 个参数。
这里,还需要记得添加 HTTPS 服务的优雅关停代码:
// 等待中断信号优雅地关闭服务器(10 秒超时)。
//quit := make(chan os.Signal, 1)
quit := make(chan os.Signal)
......
if err := httpssrv.Shutdown(ctx); err != nil {
log.Errorw("Secure Server forced to shutdown", "err", err)
return err
}
......
为了使代码看起来更加简洁,这里我重构了 internal/miniblog/miniblog.go
文件中的代码,将启动 HTTP 服务的代码和启动 HTTPS 服务的代码分别放在了 startInsecureServer
和 startSecureServer
函数中,以做到函数级别的隔离,并且使主流程看起来更清晰。
开发过程中,不断重构、优化你的代码是一个很正常,并且非常有意义的工作。
编译并测试
$ make
$ make ca # 生成根证书、服务端证书、服务端私钥
$ _output/miniblog -c configs/miniblog.yaml
新打开一个 Linux 终端。并执行以下操作:
- 创建一个测试用户。
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
- 登录测试用户。
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. 通过 HTTPS 协议访问 miniblog。不指定根证书,无法认证服务端证书报错
$ curl -XGET -H"Authorization: Bearer $token" https://127.0.0.1:8443/v1/users/catest
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
# 2. 读取根证书,并使用根证书认证服务端
$ curl -XGET --cacert _output/cert/ca.crt -H"Authorization: Bearer $token" https://127.0.0.1:8443/v1/users/catest
{"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"}
# 3. 忽略 HTTPS 证书参数,指定跳过 SSL 检测
$ curl -XGET -k -H"Authorization: Bearer $token" https://127.0.0.1:8443/v1/users/catest
{"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 通信。