4. TLS/SSL 单向认证:生成自签名证书、服务端配置TLS、客户端配置TLS、验证加密通信

在 gRPC 生产环境中,传输层安全(TLS/SSL)是保护通信机密性与完整性的第一道防线。本章将引导你完成从证书生成到双向验证的完整流程,重点聚焦于单向认证模式——即客户端验证服务端身份,而服务端不验证客户端身份。这是 gRPC 加密通信中最常见、最基础的部署形态。

学习目标:掌握 OpenSSL 自签名证书生成方法;理解 gRPC 服务端与客户端 TLS 配置的核心参数;能够通过抓包工具验证通信是否已加密。

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
重要提示:CN(Common Name)字段必须与客户端连接时使用的地址一致。如果客户端通过 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 握手的关键步骤,帮助你理解客户端如何验证服务端身份。

单向认证 TLS 握手流程 gRPC 客户端 gRPC 服务端 ① ClientHello(支持的 TLS 版本、密码套件) ② ServerHello + 服务端证书(server.crt) ③ 使用 ca.crt 验证证书 ④ 生成预主密钥,用服务端公钥加密发送 ⑤ 解密预主密钥,生成会话密钥 ⑥ 加密的 gRPC 数据流(AES-GCM) 客户端不提供证书,服务端不验证客户端身份
关键点:在步骤③中,客户端使用 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))
最佳实践:生产环境中切勿将私钥硬编码在代码中。建议使用环境变量、Vault、KMS 或 Kubernetes Secret 管理私钥。同时始终设置 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
生产环境提醒:自签名证书仅适用于开发与测试。生产环境应使用 Let's Encrypt、商业 CA 或内部 PKI 系统签发的证书。证书续期策略也需提前规划,避免因证书过期导致服务中断。