4. TLS/SSL 单向认证:生成自签名证书、服务端配置TLS、客户端配置TLS、验证加密通信
在 gRPC 生产环境中,传输层安全(TLS/SSL)是保护通信机密性与完整性的第一道防线。本章将引导你完成从证书生成到双向验证的完整流程,重点聚焦于单向认证模式——即客户端验证服务端身份,而服务端不验证客户端身份。这是 gRPC 加密通信中最常见、最基础的部署形态。
4.1 单向认证 vs 双向认证
在开始操作之前,先明确两种认证模式的区别,以便你根据实际场景做出选择。
| 对比维度 | 单向认证(Server-side TLS) | 双向认证(mTLS) |
|---|---|---|
| 验证方向 | 客户端验证服务端证书 | 双方互相验证证书 |
| 证书数量 | 仅服务端需要证书 | 服务端 + 客户端都需要证书 |
| 配置复杂度 | 低 | 中 |
| 适用场景 | 公开 API、微服务对外暴露 | 内部服务间高安全通信、金融级场景 |
| gRPC 配置 | 服务端配置 TLS,客户端配置根证书 | 双方均需配置证书与私钥 |
本章仅涉及单向认证,双向认证将在后续章节中单独讲解。
4.2 生成自签名证书
自签名证书用于开发与测试环境。生产环境应使用受信任的 CA 签发证书,但自签名证书的生成流程与配置逻辑完全一致。
4.2.1 生成 CA 根证书
CA(Certificate Authority)根证书用于签发服务端证书。在单向认证中,客户端需要持有 CA 根证书来验证服务端证书的合法性。
# 1. 生成 CA 私钥
openssl genrsa -out ca.key 2048
# 2. 生成 CA 自签名证书(有效期 3650 天)
openssl req -x509 -new -nodes -key ca.key -subj "/CN=MyCA" \
-days 3650 -out ca.crt
4.2.2 生成服务端证书
服务端证书需要由 CA 签发,并包含服务端的域名或 IP 地址。
# 1. 生成服务端私钥
openssl genrsa -out server.key 2048
# 2. 生成证书签名请求(CSR)
openssl req -new -key server.key -subj "/CN=localhost" \
-out server.csr
# 3. 使用 CA 签发服务端证书(有效期 365 天)
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365
localhost 连接,则 CN 应为 localhost;如果通过 IP 连接,则 CN 应为该 IP 地址。否则 TLS 握手会因主机名不匹配而失败。
4.2.3 证书文件清单
生成完成后,你将获得以下文件:
| 文件 | 用途 | 保密性 |
|---|---|---|
ca.crt |
CA 根证书,客户端用于验证服务端 | 公开 |
ca.key |
CA 私钥,用于签发证书 | 严格保密 |
server.crt |
服务端证书,包含公钥 | 公开 |
server.key |
服务端私钥,用于 TLS 握手 | 严格保密 |
4.3 单向认证 TLS 握手流程
下图展示了 gRPC 单向认证中 TLS 握手的关键步骤,帮助你理解客户端如何验证服务端身份。
ca.crt 验证服务端证书的签名。如果证书过期、CN 不匹配或签名无效,TLS 握手将失败,gRPC 连接会立即报错。
4.4 服务端配置 TLS
在 Go gRPC 服务端中,通过 credentials.NewServerTLSFromFile 或 credentials.NewTLS 加载证书和私钥。
4.4.1 使用文件加载(推荐开发环境)
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func main() {
// 加载服务端证书和私钥
creds, err := credentials.NewServerTLSFromFile(
"server.crt", // 证书文件路径
"server.key", // 私钥文件路径
)
if err != nil {
log.Fatalf("加载 TLS 证书失败: %v", err)
}
// 创建 gRPC 服务端,传入 TLS 凭证
s := grpc.NewServer(grpc.Creds(creds))
// 注册服务...
// pb.RegisterYourServiceServer(s, &server{})
lis, _ := net.Listen("tcp", ":50051")
s.Serve(lis)
}
4.4.2 使用内存加载(推荐生产环境)
import (
"crypto/tls"
"google.golang.org/grpc/credentials"
)
// 从环境变量或密钥管理服务读取证书内容
certPEM := []byte(`-----BEGIN CERTIFICATE-----
... 服务端证书内容 ...
-----END CERTIFICATE-----`)
keyPEM := []byte(`-----BEGIN RSA PRIVATE KEY-----
... 服务端私钥内容 ...
-----END RSA PRIVATE KEY-----`)
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
log.Fatalf("解析证书对失败: %v", err)
}
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12, // 强制最低 TLS 1.2
})
s := grpc.NewServer(grpc.Creds(creds))
MinVersion: tls.VersionTLS12,禁用不安全的 SSLv3/TLS 1.0。
4.5 客户端配置 TLS
客户端需要加载 CA 根证书来验证服务端证书的合法性。在单向认证中,客户端不需要提供自己的证书。
4.5.1 使用文件加载
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func main() {
// 加载 CA 根证书,用于验证服务端
creds, err := credentials.NewClientTLSFromFile(
"ca.crt", // CA 根证书路径
"", // ServerName 留空,由 gRPC 自动从目标地址提取
)
if err != nil {
log.Fatalf("加载 CA 证书失败: %v", err)
}
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(creds),
)
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
// 使用 conn 创建客户端...
}
4.5.2 使用内存加载
import (
"crypto/tls"
"crypto/x509"
"google.golang.org/grpc/credentials"
)
// CA 根证书内容
caCertPEM := []byte(`-----BEGIN CERTIFICATE-----
... CA 证书内容 ...
-----END CERTIFICATE-----`)
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(caCertPEM) {
log.Fatalf("添加 CA 证书失败")
}
creds := credentials.NewTLS(&tls.Config{
RootCAs: certPool,
MinVersion: tls.VersionTLS12,
})
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(creds),
)
4.5.3 ServerName 覆盖
如果服务端证书的 CN 与客户端连接地址不一致,可以通过 ServerName 字段覆盖验证目标:
creds := credentials.NewTLS(&tls.Config{
RootCAs: certPool,
ServerName: "myservice.internal", // 覆盖为证书中的 CN
MinVersion: tls.VersionTLS12,
})
4.6 验证加密通信
配置完成后,需要通过实际测试确认 TLS 加密已生效。
4.6.1 使用 gRPC 客户端测试
// 正常调用 gRPC 方法
resp, err := client.SayHello(ctx, &pb.HelloRequest{Name: "Alice"})
if err != nil {
log.Fatalf("调用失败: %v", err)
}
log.Printf("响应: %s", resp.Message)
如果 TLS 配置正确,调用将正常返回。如果证书有问题,会看到类似以下错误:
rpc error: code = Unavailable desc = connection closed
// 或
x509: certificate is valid for example.com, not localhost
4.6.2 使用 tcpdump 抓包验证
通过抓包确认传输内容是否加密:
# 抓取 50051 端口的流量
sudo tcpdump -i lo0 -X port 50051
# 在另一个终端运行 gRPC 客户端
go run client/main.go
在抓包输出中,如果看到类似 0x17 0x03 0x03 的 TLS 记录层头部,且后续内容为不可读的二进制数据,说明通信已加密。如果看到明文 protobuf 内容(如 HelloRequest 的字段名),则说明未启用 TLS。
4.6.3 使用 openssl s_client 验证
# 连接到 gRPC 服务端,查看证书链
openssl s_client -connect localhost:50051 -showcerts
# 输出中应包含:
# Certificate chain
# 0 s:/CN=localhost
# i:/CN=MyCA
# ---
# SSL handshake has read 1234 bytes and written 456 bytes
# ---
# New, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
- ✅ 服务端启动日志无 TLS 相关错误
- ✅ 客户端调用成功返回预期结果
- ✅ 抓包确认传输内容为 TLS 加密数据
- ✅
openssl s_client显示证书链完整、TLS 版本 ≥ 1.2 - ✅ 故意使用错误 CA 证书时,客户端应报错
4.7 常见错误与排查
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
x509: certificate is valid for ... not ... |
证书 CN 与连接地址不匹配 | 重新生成证书,CN 设为正确域名/IP;或使用 ServerName 覆盖 |
x509: certificate has expired or is not yet valid |
证书已过期或未到生效时间 | 检查系统时间;重新生成证书并延长有效期 |
tls: first record does not look like a TLS handshake |
服务端未启用 TLS,但客户端以 TLS 方式连接 | 检查服务端是否配置了 grpc.Creds |
connection closed |
TLS 握手失败,原因多样 | 查看服务端日志;使用 openssl s_client 诊断 |
ca certificate pool is empty |
客户端未加载 CA 根证书 | 确保 credentials.NewClientTLSFromFile 路径正确 |
4.8 完整示例项目结构
以下是本章示例的推荐文件组织方式:
tls-demo/
├── certs/ # 证书目录
│ ├── ca.crt
│ ├── ca.key
│ ├── server.crt
│ └── server.key
├── server/
│ └── main.go # 服务端代码
├── client/
│ └── main.go # 客户端代码
├── proto/
│ ├── hello.proto
│ └── hello.pb.go
└── go.mod