Spring Cloud Gateway 在 3.1.x 版本中增加了針對 gRPC 的網關代理功能支持,本片文章描述一下如何實現相關支持.本文主要基于 Spring Cloud Gateway 的 官方文檔 進行一個實踐練習。有興趣的可以翻看官方文檔。
由于 Grpc 是基于 HTTP2 協議進行傳輸的,因此 Srping Cloud Gateway 在支持了 HTTP2 的基礎上天然支持對 Grpc 服務器的代理,只需要在現有代理基礎上針對 grpc 協議進行一些處理即可。
以下為實現步驟,這里提供了示例代碼,可以按需取用.
生成服務器證書
由于 Grpc 協議使用了 Http2 作為通信協議, Http2 在正常情況下是基于 TLS 層上進行通信的,這就要求我們需要配置服務器證書,這里為了測試,使用腳本生成了一套 CA 證書:
#!/bin/bash
# 指定生成證書的目錄
dir=$(dirname "$0")/../resources/x509
[ -d "$dir" ] && find "$dir" -type f -exec rm -rf {} \;
mkdir -p "$dir"
pushd "$dir" || exit
# 生成.key 私鑰文件 和 csr 證書簽名請求文件
openssl req -new -nodes -sha256 -newkey rsa:2048 -keyout ca.key -out ca.csr \
-subj "/C=CN/ST=Zhejiang/L=Hangzhou/O=Ghimi Technology/OU=Ghimi Cloud/CN=ghimi.top"
# 生成自簽名 .crt 證書文件
openssl x509 -req -in ca.csr -key ca.key -out ca.crt -days 3650
# 生成服務器私鑰文件 和 csr 證書請求文件(私鑰簽名文件)
openssl req -new -nodes -sha256 -newkey rsa:2048 -keyout server.key -out server.csr \
-subj "/C=CN/ST=Zhejiang/L=Hangzhou/O=Ghimi Technology/OU=Ghimi Blog/CN=blog.ghimi.top"
# 3. 生成 server 證書,由 ca證書頒發
openssl x509 -req -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -extensions SAN \
-extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:dns.ghimi.top,IP:127.0.0.1,IP:::1"))
# 將 crt 證書轉換為 pkcs12 格式,生成 server.p12 文件,密碼 123456
openssl pkcs12 -export -in server.crt -inkey server.key -CAfile ca.crt \
-password pass:123456 -name server -out server.p12
# 導出服務器證書和證書私鑰為 java keystore 格式 server.jks 為最終的導出結果 密碼 123456
keytool -importkeystore -srckeystore server.p12 -destkeystore server.jks \
-srcstoretype pkcs12 -deststoretype jks -srcalias server -destalias server \
-deststorepass 123456 -srcstorepass 123456
# 將 ca 證書導入到 server.jks 中
keytool -importcert -keystore server.jks -file ca.crt -alias ca -storepass 123456 -noprompt
popd || exit
構建 Grpc 服務
首先我們需要創建一個 Maven 工程,并編寫 gRPC 相關的服務器代碼:
添加 gRPC 所需要的相關依賴:
<!-- grpc 關鍵依賴-->
io.grpc:grpc-netty-shaded:jar:1.64.0:runtime -- module io.grpc.netty.shaded [auto]
io.grpc:grpc-protobuf:jar:1.64.0:compile -- module io.grpc.protobuf [auto]
io.grpc:grpc-stub:jar:1.64.0:compile -- module io.grpc.stub [auto]
io.grpc:grpc-netty:jar:1.64.0:compile -- module io.grpc.netty [auto]
用 protobuf 生成一個 Java gRPC模板:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "service";message HelloReq {string name = 1;
}
message HelloResp {string greeting = 1;
}
service HelloService {rpc hello(HelloReq) returns (HelloResp);
}
然后在 pom.xml 添加 prptobuf 生成插件:
<!-- project.build.plugins -->
<plugin><groupId>org.xolstice.maven.plugins</groupId><artifactId>protobuf-maven-plugin</artifactId><version>0.6.1</version><configuration><protocArtifact>com.google.protobuf:protoc:3.25.1:exe:${os.detected.classifier}</protocArtifact><pluginId>grpc-java</pluginId><pluginArtifact>io.grpc:protoc-gen-grpc-java:1.64.0:exe:${os.detected.classifier}</pluginArtifact><!--設置grpc生成代碼到指定路徑--><!--<outputDirectory>${project.build.sourceDirectory}</outputDirectory>--><!--生成代碼前是否清空目錄--><clearOutputDirectory>true</clearOutputDirectory></configuration><executions><execution><goals><goal>compile</goal><goal>compile-custom</goal></goals></execution></executions>
</plugin>
注意在指定 protoc 的版本時要和上面 grpc 依賴的 protobuf 版本保持一致,否則可能會出現類找不到的報錯。
然后執行執行 Maven 命令生成 Protobuf 對應的 Java 代碼:
mvn protobuf:compile protobuf:compile-custom
之后就可以基于生成的 Protobuf Java 代碼編寫一個 gRPC Server 了 :
public static void main(String[] args) throws IOException, InterruptedException {TlsServerCredentials.Builder tlsBuilder = TlsServerCredentials.newBuilder();File serverCert = new ClassPathResource("/x509/server.crt").getFile();File serverKey = new ClassPathResource("/x509/server.key").getFile();File caCert = new ClassPathResource("/x509/ca.crt").getFile();ServerCredentials credentials = tlsBuilder.trustManager(caCert).keyManager(serverCert, serverKey).build();// ServerCredentials credentials = InsecureServerCredentials.create(); // 不建議使用,非常坑Server server = Grpc.newServerBuilderForPort(443, credentials).addService(new HelloImpl()).build();server.start().awaitTermination();
}static class HelloImpl extends HelloServiceGrpc.HelloServiceImplBase {@Overridepublic void hello(HelloReq request, StreamObserver<HelloResp> responseObserver) {String msg = "hello " + request.getName() + " from server";System.out.println("server received a req,reply: " + msg);HelloResp res = HelloResp.newBuilder().setGreeting(msg).build();responseObserver.onNext(res);responseObserver.onCompleted();}
}
嘗試啟動 GrpcServer ,檢查端口是否已被監聽,當前端口綁定在 443 上,這里 GrpcServer 的服務器證書一定要配置.
編寫 GrpcClient 代碼:
public static void main(String[] args) throws InterruptedException, IOException {// 當服務器配置了證書時需要指定 ca 證書TlsChannelCredentials.Builder tlsBuilder = TlsChannelCredentials.newBuilder();File caCert = new ClassPathResource("/x509/ca.crt").getFile();ChannelCredentials credentials = tlsBuilder.trustManager(caCert).build();// 不做服務器證書驗證時使用這個// ChannelCredentials credentials = InsecureChannelCredentials.create();ManagedChannelBuilder<?> builder = Grpc.newChannelBuilder("127.0.0.1:7443", credentials);ManagedChannel channel = builder.build();HelloServiceGrpc.HelloServiceBlockingStub stub = HelloServiceGrpc.newBlockingStub(channel);service.HelloReq.Builder reqBuilder = service.HelloReq.newBuilder();HelloResp resp = stub.hello(reqBuilder.setName("ghimi").build());System.out.printf("success greeting from server: %s", resp.getGreeting());channel.shutdown().awaitTermination(5, TimeUnit.MINUTES);
}
執行 GrpcClient,調用 443 端口的 GrpcServer 查看執行效果:
現在我們就可以開發 Spring Cloud Gateway 了,首先添加依賴,我這里添加了 spring-cloud-starter-gateway:3.1.9 版本(為了適配 Java8,已經升級 Java11 的可以提升至更高版本)。
org.springframework.cloud:spring-cloud-starter-gateway:3.1.9
編寫 GrpcGateway 啟動類:
@SpringBootApplication
public class GrpcGateway {public static void main(String[] args) {ConfigurableApplicationContext run = SpringApplication.run(GrpcGateway.class, args);}
}
先不做配置嘗試運行一下,看下是否能夠正常運行:
可以看到成功監聽到了 8080 端口,這是 Spring Cloud Gateway 的默認監聽端口,現在我們在 /src/main/resources/
目錄下添加 application.yml
配置,配置代理 grpc 端口:
server:port: 7443 #端口號http2:enabled: truessl:enabled: truekey-store: classpath:x509/server.p12key-store-password: 123456key-store-type: pkcs12key-alias: server
spring:application:name: scg_grpccloud:gateway: #網關路由配置httpclient:ssl:use-insecure-trust-manager: true
# trustedX509Certificates:
# - classpath:x509/ca.crtroutes:- id: user-grpc #路由 id,沒有固定規則,但唯一,建議與服務名對應uri: https://[::1]:443 #匹配后提供服務的路由地址predicates:#以下是斷言條件,必選全部符合條件- Path=/** #斷言,路徑匹配 注意:Path 中 P 為大寫- Header=Content-Type,application/grpcfilters:- AddResponseHeader=X-Request-header, header-value
添加 application.yml
后,重啟 Spring Cloud Gateway
嘗試用 GrpcClient 調用 7443 代理端口,可以看到請求成功:
報錯分析
GrpcServer 和 GrpcClient 如果都配置了 InsecureServerCredentials 的情況下, GrpcClient 可以直接調用 GrpcServer 成功:
GrpcServer
TlsServerCredentials.Builder tlsBuilder = TlsServerCredentials.newBuilder();
ServerCredentials credentials = InsecureServerCredentials.create(); // 配置通過 h2c(http2 clear text) 協議訪問
Server server = Grpc.newServerBuilderForPort(443, credentials).addService(new HelloImpl()).build();
server.start().awaitTermination();
GrpcClient
ChannelCredentials credentials = InsecureChannelCredentials.create(); // 通過 h2c 協議訪問 GrpcServer
tlsBuilder.requireFakeFeature();
ManagedChannelBuilder<?> builder = Grpc.newChannelBuilder("127.0.0.1:443", credentials);
ManagedChannel channel = builder.build();
HelloServiceGrpc.HelloServiceBlockingStub stub = HelloServiceGrpc.newBlockingStub(channel);
service.HelloReq.Builder reqBuilder = service.HelloReq.newBuilder();
HelloResp resp = stub.hello(reqBuilder.setName("ghimi").build());
System.out.printf("success greeting from server: %s\n", resp.getGreeting());
channel.shutdown().awaitTermination(5, TimeUnit.MINUTES);
此時使用 GrpcClient 調用 GrpcServer ,可以調用成功:
但是,如果中間添加了 Spring Cloud Gateway 的話, Grpc Server 和 Grpc Client 就都不能使用 InsecureCredentials 了, Spring Cloud Gateway 在這種場景下無論與 client 還是和 server 通信都會由于不識別的協議格式而報錯:
如果 GrpcServer 沒有配置服務器證書而是使用了 InsecureServerCredentials.create()
,GrpcClient 雖然不使用證書訪問能夠直接驗證成功,但是如果中間通過 GrpcGateway 的話這種機制就有可能出現問題,因為 GrpcGateway 與 GrpcServer 之間的通信是基于 Http2 的,而非 Grpc 特定的協議,在 GrpcServer 沒有配置服務器證書的情況下處理的包可能會導致 GrpcGateway 無法識別,但是如果 GrpcServer 配置了證書后 GrpcGateway 就能夠正常驗證了。
GrpcServer 在沒有配置證書的情況下通過 Srping Cloud Gateway 的方式進行代理,并且 Spring Cloud Gateway 的 spring.cloud.gateway.http-client.ssl.use-insecure-trust-manager=true
的場景下 GrpcClient 訪問 Spring Cloud Gateway 會報錯:
GrpcClient 報錯信息:
Exception in thread "main" io.grpc.StatusRuntimeException: UNKNOWN: HTTP status code 500
invalid content-type: application/json
headers: Metadata(:status=500,x-request-header=header-value,content-type=application/json,content-length=146)
DATA-----------------------------
{"timestamp":"2024-06-28T13:18:03.455+00:00","path":"/HelloService/hello","status":500,"error":"Internal Server Error","requestId":"31f8f577/1-8"}at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:268)at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:249)at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:167)at service.HelloServiceGrpc$HelloServiceBlockingStub.hello(HelloServiceGrpc.java:160)at com.example.GrpcClient.main(GrpcClient.java:30)
報錯信息解析,GrpcClient 報錯結果來自于 Spring Cloud Gateway ,返回結果為不識別的返回內容 invalid content-type: application/json
這是由于 Spring Cloud Gateway 返回了的報錯信息是 application/json
格式的,但是 GrpcClient 通過 grpc 協議通信,因此會將錯誤格式錯誤直接返回而非正確解析錯誤信息.
GrpcGateway 報錯信息:
io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record: 00001204000000000000037fffffff000400100000000600002000000004080000000000000f0001at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1313) ~[netty-handler-4.1.100.Final.jar:4.1.100.Final]Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):*__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]*__checkpoint ? HTTP POST "/HelloService/hello" [ExceptionHandlingWebHandler]
Original Stack Trace:at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1313) ~[netty-handler-4.1.100.Final.jar:4.1.100.Final]at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1383) ~[netty-handler-4.1.100.Final.jar:4.1.100.Final]
這里就是的報錯信息是 GrpcGateway 無法正確解析來自 GrpcServer 的 http2 的包信息而產生的報錯.這是由于 GrpcGateway 與 GrpcServer 在 h2c(Http2 Clean Text) 協議上的通信格式存在差異,從而引發報錯.
最后是來自 GrpcServer 的報錯:
6月 28, 2024 9:18:03 下午 io.grpc.netty.shaded.io.grpc.netty.NettyServerTransport notifyTerminated
信息: Transport failed
io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception: HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 16030302650100026103036f6977c824c322105c600bd1dbat io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception.connectionError(Http2Exception.java:109)at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.readClientPrefaceString(Http2ConnectionHandler.java:321)at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.decode(Http2ConnectionHandler.java:247)at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler.decode(Http2ConnectionHandler.java:453)
這里就是 GrpcServer 與 GrpcGateway 通信過程由于協議包無法識別導致通信終止,從而引發報錯.
報錯場景2
在 Spring Cloud Gateway 的 application.yml 中同時配置了 use-insecure-trust-manager: true
和 trustedX509Certificates
導致的報錯:
spring:cloud:gateway: #網關路由配置httpclient:ssl:use-insecure-trust-manager: truetrustedX509Certificates:- classpath:x509/ca.crt
use-insecure-trust-manager: true
表示在于 GrpcServer 通信的過程中不會驗證服務器證書,這樣如果證書存在什么問題的情況下就不會引發報錯了,但是在同時配置了 use-insecure-trust-manager: true
和 trustedX509Certificates
的情況下 use-insecure-trust-manager: true
選項是不生效的,Spring Cloud Gateway 會還是嘗試通過配置的 ca 證書去驗證服務器證書,從而引發報錯,因此不要同時配置 use-insecure-trust-manager: true
和 trustedX509Certificates
這兩個選項。
# 同時配置了 `use-insecure-trust-manager: true` 和 `trustedX509Certificates` 后服務證書校驗失敗報錯
javax.net.ssl.SSLHandshakeException: No subject alternative names matching IP address 0:0:0:0:0:0:0:1 foundat java.base/sun.security.ssl.Alert.createSSLException(Alert.java:130) ~[na:na]Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):*__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]*__checkpoint ? HTTP POST "/HelloService/hello" [ExceptionHandlingWebHandler]
Original Stack Trace:
客戶端通常情況下只需要配置 ca 證書,用于驗證服務器證書,但是驗證服務器證書這一步是可以跳過的,在一些場景下服務器證書的校驗比較嚴格的時候容易出問題,此時可以選擇不進行服務器證書校驗,在 Spring Cloud Gateway 代理訪問 GrpcServer 時,可以為 Spring Cloud Gateway 配置 use-insecure-trust-manager: true
來取消對 GrpcServer 的強驗證。
No subject alternative names matching IP address 0:0:0:0:0:0:0:1 found
這個問題就是在校驗服務器證書時,由于服務器證書校驗失敗導致的報錯了,通常情況下, client 會校驗服務器的FQDN域名信息是否與請求的連接一致:
# 請求服務器證書
openssl req -new -nodes -sha256 -newkey rsa:2048 -keyout server.key -out server.csr \
-subj "/C=CN/ST=Zhejiang/L=Hangzhou/O=Ghimi Technology/OU=Ghimi Blog/CN=blog.ghimi.top"
上面是在使用命令生成服務器證書時配置的信息,其中 CN=blog.ghimi.top
就是我配置的域名信息,這就要求我的 GrpcServer 的 ip 地址綁定了這個域名,然后 GrpcClient 通過這個域名訪問:
ManagedChannelBuilder<?> builder = Grpc.newChannelBuilder("blog.ghimi.top:7443", credentials);
在這種情況下 GrpcClient 會拿服務器返回的證書與當前連接信息進行比較,如果一致則服務器驗證成功,否則驗證失敗并拋出異常.
在 GrpcServer 只有 ip 地址沒有域名的情況下,基于域名的驗證就不生效了,此時去做證書驗證就一定會報錯:
# 同時配置了 `use-insecure-trust-manager: true` 和 `trustedX509Certificates` 后服務證書校驗失敗報錯
javax.net.ssl.SSLHandshakeException: No subject alternative names matching IP address 0:0:0:0:0:0:0:1 foundat java.base/sun.security.ssl.Alert.createSSLException(Alert.java:130) ~[na:na]Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):*__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]*__checkpoint ? HTTP POST "/HelloService/hello" [ExceptionHandlingWebHandler]
Original Stack Trace:
報錯信息中提到的 subject alternative names
就是在域名失效后的另外一種驗證手段,他要求ca在簽發服務器證書時向服務器證書中添加一段附加信息,這個信息中可以添加證書的可信 ip 地址:
# 通過 ca 證書頒發服務器證書
openssl x509 -req -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -extensions SAN \
-extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:dns.ghimi.top,IP:127.0.0.1"))
上面腳本中的 IP:127.0.0.1
就是添加的可信地址,我們可以同時添加多個服務器地址,以上面的報錯為例,我們只需要在生成服務器證書的時候添加 ::1
的本地 ipv6 地址即可修復該錯誤:
openssl x509 -req -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -extensions SAN \
-extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:dns.ghimi.top,IP:127.0.0.1,IP:::1"))
報錯場景4 使用 pkcs12 配置了多張自簽名 ca 證書識別失效問題
解決方案,改為使用 Java Keystore 格式的證書即可修復.
參考資料
- Spring Cloud Gateway and gRPC
- spring-cloud-gateway-grpc
- gRPC-Spring-Boot-Starter 文檔
- rx-java
- Working with Certificates and SSL
- 介紹一下 X.509 數字證書中的擴展項 subjectAltName