使用gRPC作為雲平臺和移動前端的連線方式,網路安全應該是必須考慮的一個重點。gRPC是支援ssl/tls安全通訊機制的。用了一個週末來研究具體使用方法,實際上是一個週末的挖坑填坑過程。把這次經歷記錄下來與各位分享。
gRPC的ssl/tls的原理是在服務端安裝安全證照公用certificate和私鑰key, 在客戶端安裝公共證照就可以了,gRPC程式碼是這樣寫的:
// Server
SslContext sslContext = SslContextBuilder.forServer(
new File("/Users/u/Desktop/api.grpc/src/main/resources/my-public-key-cert.pem"),
new File("/Users/u/Desktop/api.grpc/src/main/resources/my-private-key.pem"))
.build();
server = NettyServerBuilder.forPort(port).sslContext(sslContext)
.addService(GreeterGrpc.bindService(new GreeterImpl())).build()
.start();
/ Client
SslContext sslContext = SslContextBuilder.forClient().trustManager(new File(
"/Users/u/Desktop/api.grpc/src/main/resources/my-public-key-cert.pem")).build();
channel = NettyChannelBuilder.forAddress(host, port)
.sslContext(sslContext)
.build();
blockingStub = GreeterGrpc.newBlockingStub(channel);
先構建SslContextBuilder,然後在構建NettyServerBuilder和NettyChannelBuilder時加入sslContext。上面的my-public-key-cert.pem,my-private_key.pem是用openssl產生的:
openssl req -x509 -newkey rsa:4096 -keyout my-private-key.pem -out my-public-key-cert.pem -days 365 -nodes -subj '/CN=localhost'
不過使用這個證照和私鑰測試時出現了錯誤:
Jan 27, 2019 4:08:22 PM io.grpc.netty.GrpcSslContexts defaultSslProvider
INFO: netty-tcnative unavailable (this may be normal)
java.lang.ClassNotFoundException: io.netty.internal.tcnative.SSL
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
...
INFO: Jetty ALPN unavailable (this may be normal)
java.lang.ClassNotFoundException: org/eclipse/jetty/alpn/ALPN
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
...
仔細研究了一下github上的gRPC-java說明檔案SECURITY.MD,感覺應該是grpc和netty版本問題,特別是下面這幾個依賴:
Find the dependency tree (e.g., mvn dependency:tree), and look for versions of:
io.grpc:grpc-netty
io.netty:netty-handler (really, make sure all of io.netty except for netty-tcnative has the same version)
io.netty:netty-tcnative-boringssl-static:jar
...
grpc-netty version netty-handler version netty-tcnative-boringssl-static version
1.0.0-1.0.1 4.1.3.Final 1.1.33.Fork19
1.0.2-1.0.3 4.1.6.Final 1.1.33.Fork23
1.1.x-1.3.x 4.1.8.Final 1.1.33.Fork26
1.4.x 4.1.11.Final 2.0.1.Final
1.5.x 4.1.12.Final 2.0.5.Final
1.6.x 4.1.14.Final 2.0.5.Final
1.7.x-1.8.x 4.1.16.Final 2.0.6.Final
1.9.x-1.10.x 4.1.17.Final 2.0.7.Final
1.11.x-1.12.x 4.1.22.Final 2.0.7.Final
1.13.x 4.1.25.Final 2.0.8.Final
1.14.x-1.15.x 4.1.27.Final 2.0.12.Final
1.16.x-1.17.x 4.1.30.Final 2.0.17.Final
1.18.x-1.19.x 4.1.32.Final 2.0.20.Final
1.20.x-1.21x 4.1.34.Final 2.0.22.Final
1.22.x- 4.1.35.Final 2.0.25.Final
解決問題必須先搞清楚這些庫的版本。這個可以用sbt外掛sbt-dependency-graph。加入project/plugins.sbt:
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.15")
addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.21")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.9.0-M6"
在sbt中執行dependencyTree:
~/scala/intellij/learn-grpc> sbt
[info] Loading settings for project global-plugins from idea.sbt ...
sbt:learn-grpc> dependencyTree
[info] | | +-com.google.protobuf:protobuf-java:3.7.1
[info] | | +-io.grpc:grpc-api:1.21.0
[info] | | +-io.netty:netty-handler:4.1.34.Final
[info] | | | +-io.netty:netty-buffer:4.1.34.Final
[info] | | | | +-io.netty:netty-common:4.1.34.Final
...
好像缺失了io.netty:netty-tcnative-boringssl-static:jar,按照對應的gRPC版本在build.sbt里加上:
name := "learn-grpc"
version := "0.1"
scalaVersion := "2.12.8"
scalacOptions += "-Ypartial-unification"
val akkaversion = "2.5.23"
libraryDependencies := Seq(
"com.typesafe.akka" %% "akka-cluster-metrics" % akkaversion,
"com.typesafe.akka" %% "akka-cluster-sharding" % akkaversion,
"com.typesafe.akka" %% "akka-persistence" % akkaversion,
"com.lightbend.akka" %% "akka-stream-alpakka-cassandra" % "1.0.1",
"org.mongodb.scala" %% "mongo-scala-driver" % "2.6.0",
"com.lightbend.akka" %% "akka-stream-alpakka-mongodb" % "1.0.1",
"com.typesafe.akka" %% "akka-persistence-query" % akkaversion,
"com.typesafe.akka" %% "akka-persistence-cassandra" % "0.97",
"com.datastax.cassandra" % "cassandra-driver-core" % "3.6.0",
"com.datastax.cassandra" % "cassandra-driver-extras" % "3.6.0",
"ch.qos.logback" % "logback-classic" % "1.2.3",
"io.monix" %% "monix" % "3.0.0-RC2",
"org.typelevel" %% "cats-core" % "2.0.0-M1",
"io.grpc" % "grpc-netty" % scalapb.compiler.Version.grpcJavaVersion,
"io.netty" % "netty-tcnative-boringssl-static" % "2.0.22.Final",
"com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf",
"com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion
)
// (optional) If you need scalapb/scalapb.proto or anything from
// google/protobuf/*.proto
//libraryDependencies += "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf"
PB.targets in Compile := Seq(
scalapb.gen() -> (sourceManaged in Compile).value
)
enablePlugins(JavaAppPackaging)
試了一下啟動服務,現在不出錯誤了(構建sslContext成功了)。不過客戶端在使用了證照後仍然無法連線到服務端。沒辦法,又要再去查資料了。看來現在應該是證照的問題了。先看看是不是因為使用的證照是自籤的self-signed-certificate。grpc-java裡提供了一些測試用的證照和私鑰和說明文件。在測試程式裡使用了它們提供的server1.pem,server1.key,ca.pem:
package learn.grpc.server
import io.grpc.{ServerBuilder,ServerServiceDefinition}
import io.grpc.netty.NettyServerBuilder
import java.io._
trait gRPCServer {
val serverCrtFile = new File("/Users/tiger/certs/server1.pem")
val serverKeyFile = new File("/Users/tiger/certs/server1.key")
def runServer(service: ServerServiceDefinition): Unit = {
val server = NettyServerBuilder
.forPort(50051)
.addService(service)
.useTransportSecurity(serverCrtFile,serverKeyFile)
.build
.start
// make sure our server is stopped when jvm is shut down
Runtime.getRuntime.addShutdownHook(new Thread() {
override def run(): Unit = server.shutdown()
})
server.awaitTermination()
}
}
...
package learn.grpc.sum.one2many.client
import java.io.File
import io.grpc.stub.StreamObserver
import demo.services.sum._
import io.grpc.netty.{GrpcSslContexts, NegotiationType, NettyChannelBuilder}
object One2ManyClient {
def main(args: Array[String]): Unit = {
val clientCrtFile = new File("/Users/tiger/certs/ca.pem") // new File(getClass.getClassLoader.getResource("badserver.pem").getPath)
val sslContextBuilder = GrpcSslContexts.forClient().trustManager(clientCrtFile)
//build connection channel
val channel = NettyChannelBuilder
.forAddress("192.168.0.189",50051)
.negotiationType(NegotiationType.TLS)
.sslContext(sslContextBuilder.build())
.overrideAuthority("foo.test.google.fr")
.build()
// get asyn stub
val client: SumOneToManyGrpc.SumOneToManyStub = SumOneToManyGrpc.stub(channel)
// prepare stream observer
val streamObserver = new StreamObserver[SumResponse] {
override def onError(t: Throwable): Unit = println(s"error: ${t.getMessage}")
override def onCompleted(): Unit = println("Done incrementing !!!")
override def onNext(value: SumResponse): Unit = println(s"current value: ${value.currentResult}")
}
// call service with stream observer
client.addOneToMany(SumRequest().withToAdd(6),streamObserver)
// wait for async execution
scala.io.StdIn.readLine()
}
}
連線成功了。判斷正確,是證照的問題。再研究一下證照是怎麼產生的,嘗試按文件指引重新產生這些自簽證照:可惜的是好像還有些檔案是缺失的,如serial。那麼上面的.overrideAuthority("foo.test.google.fr")又是什麼意思呢?算了,以後有時間再研究吧。這次起碼證明grpc ssl/tls是可以發揮作用的。