grpc-java TLS 加密通信实战:基于 example-tls 实现单向 TLS 与双向 Mutual TLS
【免费下载链接】grpc-javaThe Java gRPC implementation. HTTP/2 based RPC项目地址: https://gitcode.com/GitHub_Trending/gr/grpc-java
gRPC 默认基于 HTTP/2 明文传输,生产环境中必须通过 TLS 保证通信的机密性与身份真实性。本指南以 grpc-java 仓库中的 examples/example-tls 示例为核心,完整讲解 Hello World 服务在 TLS 场景下的构建、证书准备、单向 TLS 与双向 mTLS 的配置方法,并深入剖析TlsServerCredentials、TlsChannelCredentials与overrideAuthority的底层实现原理。读完本文,你将掌握 grpc-java 中基于文件证书的 TLS 服务端/客户端完整配置流程,并能用三种主流构建工具(Gradle、Maven、Bazel)运行 TLS 示例。
示例概览与目录结构
example-tls是 grpc-java 仓库中的官方 TLS 演示项目,与基础 Hello World 示例的唯一区别在于:服务端与客户端在启动时加载 PEM 格式证书,并通过Grpc.newServerBuilderForPort(...)/Grpc.newChannelBuilderForAddress(...)传入 TLS 凭据。其核心源码与配置如下:
- HelloWorldServerTls.java:TLS 服务端入口,解析命令行参数并组装
TlsServerCredentials; - HelloWorldClientTls.java:TLS 客户端入口,解析命令行参数并组装
TlsChannelCredentials; - helloworld.proto:仅包含一个
Greeter.SayHello一元 RPC,与普通 Hello World 完全相同; - pom.xml、BUILD.bazel、settings.gradle:三种构建系统的配置文件。
proto 定义中java_multiple_files = true与java_package = "io.grpc.examples.helloworld"决定了生成的桩代码(GreeterGrpc、HelloRequest、HelloReply)所在的包路径,服务端与客户端的 import 均依赖这些生成类。
前置条件:gRPC Java 库与代码生成插件的安装
示例的运行依赖 grpc-java 核心库与protoc-gen-grpc-java代码生成插件,README 强烈建议先检出 git release 标签,因为发布版本已包含可用的预构建产物:
git checkout v<major>.<minor>.<patch>如果坚持使用未发布版本(例如 master HEAD),则必须先按仓库根目录下的 COMPILING.md 完整构建 gRPC Java 库,并将其连同代码生成插件安装到本地:
- 通过 Gradle 安装本地 SNAPSHOT:在仓库根目录执行
./gradlew publishToMavenLocal(对应 COMPILING.md); - 构建时如需跳过 C++ codegen 与 Android 模块,可在
<project-root>/gradle.properties中分别添加skipCodegen=true与skipAndroid=true; - 注意构建需要 JDK 8,因为测试用例依赖 TLS。
也就是说,非 release 版本下运行本文所有构建与运行命令前,必须先完成这一步。
使用 Gradle 构建并运行(推荐路径)
在examples/example-tls目录内执行:
$ ../gradlew installDist该命令会创建两个可直接执行的启动脚本,位于build/install/example-tls/bin/目录下:
hello-world-tls-serverhello-world-tls-client
与普通 Hello World 一样,示例要求先启动服务端,再启动客户端。TLS 版本只是额外增加了证书相关的命令行参数。
服务端参数说明
USAGE: HelloWorldServerTls port certChainFilePath privateKeyFilePath [trustCertCollectionFilePath] Note: You only need to supply trustCertCollectionFilePath if you want to enable Mutual TLS.参数含义如下:
| 参数 | 必选 | 说明 |
|---|---|---|
port | 是 | 服务监听端口 |
certChainFilePath | 是 | PEM 格式的证书链文件路径(服务器证书) |
privateKeyFilePath | 是 | 与证书配对的私钥文件路径 |
trustCertCollectionFilePath | 否 | CA 证书集合文件路径;仅在启用双向 mTLS 时需要 |
客户端参数说明
USAGE: HelloWorldClientTls host port [trustCertCollectionFilePath [clientCertChainFilePath clientPrivateKeyFilePath]] Note: clientCertChainFilePath and clientPrivateKeyFilePath are only needed if mutual auth is desired.| 参数 | 必选 | 说明 |
|---|---|---|
host | 是 | 服务端主机名 |
port | 是 | 服务端端口 |
trustCertCollectionFilePath | 否 | CA 证书集合文件路径;若不提供则使用系统默认证书颁发机构(system default CA) |
clientCertChainFilePath、clientPrivateKeyFilePath | 否 | 客户端证书链与私钥;仅在双向认证时需要 |
单向 TLS(无 mutual auth)
所谓单向 TLS,指仅服务端出示证书、客户端通过 CA 校验服务端身份:
# 运行服务端: ./build/install/example-tls/bin/hello-world-tls-server 50440 ../../testing/src/main/resources/certs/server1.pem ../../testing/src/main/resources/certs/server1.key # 在另一个终端运行客户端: ./build/install/example-tls/bin/hello-world-tls-client localhost 50440 ../../testing/src/main/resources/certs/ca.pem双向 TLS(Mutual Auth)
双向 TLS 要求客户端也持有证书并在握手阶段出示,服务端通过 CA 反向校验客户端身份:
# 运行服务端(追加第四个参数 trustCertCollectionFilePath,启用 mTLS): ./build/install/example-tls/bin/hello-world-tls-server 50440 ../../testing/src/main/resources/certs/server1.pem ../../testing/src/main/resources/certs/server1.key ../../testing/src/main/resources/certs/ca.pem # 在另一个终端运行客户端(追加 clientCertChainFilePath 与 clientPrivateKeyFilePath): ./build/install/example-tls/bin/hello-world-tls-client localhost 50440 ../../testing/src/main/resources/certs/ca.pem ../../testing/src/main/resources/certs/client.pem ../../testing/src/main/resources/certs/client.key测试证书与 overrideAuthority:为什么必须匹配 SAN
README 特别强调:使用仓库自带测试证书时,客户端必须通过.overrideAuthority("foo.test.google.fr")覆盖ManagedChannelBuilder(此处实际为Grpc.newChannelBuilderForAddress返回的 builder)的目标授权,以匹配测试证书 Subject Alternative Names 中的域名。
这是因为 TLS 握手时 gRPC 客户端会用authority(默认是host:port)去校验服务器证书的 SAN(Subject Alternative Name)。测试证书server1.pem的 SAN 配置在 server1-openssl.cnf 中,CN 为*.test.google.com,SAN 包含foo.test.google.fr,因此连接localhost时只有覆盖 authority 才能通过主机名校验。HelloWorldClientTls.java 中的实现如下:
ManagedChannel channel = Grpc.newChannelBuilderForAddress(host, port, tlsBuilder.build()) /* Only for using provided test certs. */ .overrideAuthority("foo.test.google.fr") .build();源码注释明确说明overrideAuthority仅用于测试证书场景。如果你使用的是由真实 CA 签发的、SAN 与主机名匹配的正式服务器证书,则不需要trustCertCollectionFilePath(直接走系统默认 CA),也不需要 overrideAuthority:
Note
trustCertCollectionFilePathis not needed if you are using system default certificate authority.Note you can use system default certificate authority if you are using a real server certificate.
仓库中的测试凭据位于 testing/src/main/resources/certs,包含ca.pem/ca.key(自签 CA)、server1.pem/server1.key(由 CA 签发、含 SAN)、client.pem/client.key(客户端证书)、以及badclient.*/badserver.*(自签错误凭据,用于负面测试)。
用 OpenSSL 生成自己的自签名证书
若不想使用仓库测试证书,可参照 testing/src/main/resources/certs/README 中记录的生成命令。核心三步流程如下:
- 生成自签 CA:
openssl req -x509 -new -newkey rsa:2048 -nodes -keyout ca.key -out ca.pem \ -config ca-openssl.cnf -days 3650 -extensions v3_req- 生成服务器私钥(PKCS#8 无加密格式,便于 gRPC 直接读取)与证书签名请求:
openssl genrsa -out server.key.rsa 2048 openssl pkcs8 -topk8 -in server.key.rsa -out server.key -nocrypt openssl req -new -key server.key -out server.csr -config server1-openssl.cnf- 用 CA 签发服务器证书(务必带上 SAN 扩展,否则客户端主机名校验会失败):
openssl x509 -req -CA ca.pem -CAkey ca.key -CAcreateserial -in server.csr \ -out server.pem -extensions req_ext -extfile server1-openssl.cnf -days 3650客户端证书的生成方式相同,仅需将 CN 设为testclient等标识。注意 gRPC 要求私钥为 PKCS#8 格式(openssl pkcs8 -topk8 ... -nocrypt生成),这正是仓库中client.key、server1.key的由来。
源码剖析:服务端如何组装 TlsServerCredentials
HelloWorldServerTls.java 的main方法按参数个数决定是否启用 mTLS:
if (args.length < 3 || args.length > 4) { System.out.println( "USAGE: HelloWorldServerTls port certChainFilePath privateKeyFilePath " + "[trustCertCollectionFilePath]\n Note: You only need to supply trustCertCollectionFilePath if you want " + "to enable Mutual TLS."); System.exit(0); } // If only providing a private key, you can use TlsServerCredentials.create() instead of // interacting with the Builder. TlsServerCredentials.Builder tlsBuilder = TlsServerCredentials.newBuilder() .keyManager(new File(args[1]), new File(args[2])); if (args.length == 4) { tlsBuilder.trustManager(new File(args[3])); tlsBuilder.clientAuth(TlsServerCredentials.ClientAuth.REQUIRE); } final HelloWorldServerTls server = new HelloWorldServerTls( Integer.parseInt(args[0]), tlsBuilder.build()); server.start(); server.blockUntilShutdown();关键点:
TlsServerCredentials.newBuilder()是 api/src/main/java/io/grpc/TlsServerCredentials.java 提供的工厂方法;keyManager(File certChain, File privateKey)(TlsServerCredentials.java)加载服务端证书链与私钥;trustManager(File rootCerts)(TlsServerCredentials.java)加载用于校验客户端证书的 CA 集合;clientAuth(TlsServerCredentials.ClientAuth.REQUIRE)(TlsServerCredentials.java)强制要求客户端出示证书——这是单向 TLS 与双向 mTLS 在服务端侧的本质区别;- 若只提供私钥而不需要额外配置,也可直接用
TlsServerCredentials.create()便捷方法,这是源码注释中明确提到的简化路径。
服务端随后通过Grpc.newServerBuilderForPort(port, creds)创建带 TLS 凭据的 ServerBuilder(HelloWorldServerTls.java),这与普通明文服务的ServerBuilder.forPort(port)形成对比——TLS 凭据必须在构建 ServerBuilder 时传入,而不是运行时动态附加。
源码剖析:客户端如何组装 TlsChannelCredentials
HelloWorldClientTls.java 的main方法通过参数个数区分三种场景(单向信任自定义 CA、双向认证、系统默认 CA):
if (args.length < 2 || args.length == 4 || args.length > 5) { System.out.println("USAGE: HelloWorldClientTls host port [trustCertCollectionFilePath " + "[clientCertChainFilePath clientPrivateKeyFilePath]]\n Note: clientCertChainFilePath and " + "clientPrivateKeyFilePath are only needed if mutual auth is desired."); System.exit(0); } // If only defaults are necessary, you can use TlsChannelCredentials.create() instead of // interacting with the Builder. TlsChannelCredentials.Builder tlsBuilder = TlsChannelCredentials.newBuilder(); switch (args.length) { case 5: tlsBuilder.keyManager(new File(args[3]), new File(args[4])); // fallthrough case 3: tlsBuilder.trustManager(new File(args[2])); // fallthrough default: } String host = args[0]; int port = Integer.parseInt(args[1]); ManagedChannel channel = Grpc.newChannelBuilderForAddress(host, port, tlsBuilder.build()) /* Only for using provided test certs. */ .overrideAuthority("foo.test.google.fr") .build();要点解析:
- 参数组合规则:
args.length == 2表示完全信任系统默认 CA(无需任何证书文件);args.length == 3表示自定义 trustManager(信任私有 CA);args.length == 5表示双向认证(追加客户端证书与私钥);args.length == 4属于非法组合,直接打印 USAGE 退出; - switch fallthrough 的巧妙复用:
case 5先加载客户端 keyManager,随后 fallthrough 到case 3加载 trustManager,一段代码覆盖两种场景; - 客户端最终通过
Grpc.newChannelBuilderForAddress(host, port, creds)建立带 TLS 的 Channel,并调用blockingStub.sayHello(request)发起 RPC;捕获StatusRuntimeException记录失败状态(HelloWorldClientTls.java); - 类似地,若只需默认配置,可使用
TlsChannelCredentials.create()便捷方法。
从源码结构看,TlsServerCredentials与TlsChannelCredentials是对称设计:服务端侧重keyManager+ 可选的trustManager+clientAuth策略,客户端侧重可选的trustManager+ 可选的keyManager。两者最终都转换为 Netty 底层的 SslContext 配置(示例的 Bazel 构建中显式依赖了io_netty_netty_tcnative_boringssl_static与io_netty_netty_tcnative_classes,见 BUILD.bazel,这是 Netty TLS 的 BoringSSL 原生实现)。
使用 Maven 构建与运行
如果偏好 Maven,example-tls同样提供了完整的 pom.xml。该 POM 通过protobuf-maven-plugin自动完成 protoc 与protoc-gen-grpc-java的代码生成(其中grpc.version为1.85.0-SNAPSHOT,protoc.version为3.25.8),并依赖grpc-protobuf、grpc-stub、grpc-netty-shaded三个核心 artifact。
在examples/example-tls目录下执行:
$ mvn verify $ # 运行服务端 $ mvn exec:java -Dexec.mainClass=io.grpc.examples.helloworldtls.HelloWorldServerTls -Dexec.args="50440 ../../testing/src/main/resources/certs/server1.pem ../../testing/src/main/resources/certs/server1.key" $ # 在另一个终端运行客户端 $ mvn exec:java -Dexec.mainClass=io.grpc.examples.helloworldtls.HelloWorldClientTls -Dexec.args="localhost 50440 ../../testing/src/main/resources/certs/ca.pem"mvn verify会先编译并执行测试,确保生成的桩代码正确;之后两次mvn exec:java分别以HelloWorldServerTls和HelloWorldClientTls作为主类启动。注意 Maven 路径下证书的相对路径../../testing/...同样以examples/example-tls为当前工作目录计算。
使用 Bazel 构建与运行
仓库根目录基于 Bazel 构建,example-tls的 BUILD.bazel 定义了proto_library→java_proto_library→java_grpc_library的生成链,以及两个java_binary目标。运行方式如下:
$ bazel build :hello-world-tls-server :hello-world-tls-client $ # 运行服务端 $ ../bazel-bin/hello-world-tls-server 50440 ../../testing/src/main/resources/certs/server1.pem ../../testing/src/main/resources/certs/server1.key $ # 在另一个终端运行客户端 $ ../bazel-bin/hello-world-tls-client localhost 50440 ../../testing/src/main/resources/certs/ca.pemBazel 生成的二进制输出到仓库根目录的bazel-bin/,因此命令中需要../bazel-bin/...前缀(相对于examples/example-tls)。另外该 BUILD 文件中两个java_binary目标均标记为testonly = 1,说明官方将示例视为测试用途,生产代码不应直接引用。
常见问题与排查思路
- 证书与主机名不匹配(UNAVAILABLE / SSL 握手失败):检查服务器证书 SAN 是否包含客户端连接的 host,或是否遗漏
.overrideAuthority(...); - 私钥格式错误:gRPC 需要 PKCS#8 无加密私钥,用
openssl pkcs8 -topk8 -in key.rsa -out key -nocrypt转换; - mTLS 客户端未提供证书:服务端
clientAuth(REQUIRE)后,若客户端只传了trustCertCollectionFilePath(args.length == 3),握手会因缺少客户端证书而失败; - 使用真实证书时的简化:真实 CA 签发的证书可省略客户端
trustCertCollectionFilePath,直接依赖系统默认 CA 存储; - 运行顺序:务必先启动服务端再启动客户端,这与普通 Hello World 示例的行为一致。
小结
本文完整还原了 examples/example-tls/README.md 的全部构建与运行流程,并深入到HelloWorldServerTls、HelloWorldClientTls的源码实现与TlsServerCredentials/TlsChannelCredentials的 Builder API。核心要点可归纳为三条:单向 TLS 只需服务端证书;双向 mTLS 需服务端clientAuth(REQUIRE)且客户端提供证书;测试证书必须配合overrideAuthority使用,而真实证书则可直接走系统默认 CA。无论使用 Gradle、Maven 还是 Bazel,example-tls都是一份可直接复制的 TLS 接入范本——只需将测试证书替换为正式签发的证书,即可无缝迁移到生产环境。
【免费下载链接】grpc-javaThe Java gRPC implementation. HTTP/2 based RPC项目地址: https://gitcode.com/GitHub_Trending/gr/grpc-java
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考