前面谈过gRPC的SSL/TLS安全机制,发现设置过程比较复杂:比如证书签名:需要服务端、客户端两头都设置等。想想实际上用JWT会更加便捷,而且更安全和功能强大,因为除JWT的加密签名之外还可以把私密的用户信息放在JWT里加密后在服务端和客户端之间传递。当然,最基本的是通过对JWT的验证机制可以控制客户端对某些功能的使用权限。
通过JWT实现gRPC的函数调用权限管理原理其实很简单:客户端首先从服务端通过身份验证获取JWT,然后在调用服务函数时把这个JWT同时传给服务端进行权限验证。客户端提交身份验证请求返回JWT可以用一个独立的服务函数实现,如下面.proto文件里的GetAuthToken:
message PBPOSCredential {
string userid = 1;
string password = 2;
}
message PBPOSToken {
string jwt = 1;
}
service SendCommand {
rpc SingleResponse(PBPOSCommand) returns (PBPOSResponse) {};
rpc GetTxnItems(PBPOSCommand) returns (stream PBTxnItem) {};
rpc GetAuthToken(PBPOSCredential) returns (PBPOSToken) {};
}
比较棘手的是如何把JWT从客户端传送至服务端,因为gRPC基本上骑劫了Request和Response。其中一个方法是通过Interceptor来截取Request的header即metadata。客户端将JWT写入metadata,服务端从metadata读取JWT。
我们先看看客户端的Interceptor设置和使用:
class AuthClientInterceptor(jwt: String) extends ClientInterceptor {
def interceptCall[ReqT, RespT](methodDescriptor: MethodDescriptor[ReqT, RespT], callOptions: CallOptions, channel: io.grpc.Channel): ClientCall[ReqT, RespT] =
new ForwardingClientCall.SimpleForwardingClientCall[ReqT, RespT](channel.newCall(methodDescriptor, callOptions)) {
override def start(responseListener: ClientCall.Listener[RespT], headers: Metadata): Unit = {
headers.put(Key.of("jwt", Metadata.ASCII_STRING_MARSHALLER), jwt)
super.start(responseListener, headers)
}
}
}
...
val unsafeChannel = NettyChannelBuilder
.forAddress("192.168.0.189",50051)
.negotiationType(NegotiationType.PLAINTEXT)
.build()
val securedChannel = ClientInterceptors.intercept(unsafeChannel, new AuthClientInterceptor(jwt))
val securedClient = SendCommandGrpc.blockingStub(securedChannel)
val resp = securedClient.singleResponse(PBPOSCommand())
身份验证请求即JWT获取是不需要Interceptor的,所以要用没有Interceptor的unsafeChannel:
//build connection channel
val unsafeChannel = NettyChannelBuilder
.forAddress("192.168.0.189",50051)
.negotiationType(NegotiationType.PLAINTEXT)
.build()
val authClient = SendCommandGrpc.blockingStub(unsafeChannel)
val jwt = authClient.getAuthToken(PBPOSCredential(userid="johnny",password="p4ssw0rd")).jwt
println(s"got jwt: $jwt")
JWT的构建和使用已经在前面的几篇博文里讨论过了:
package com.datatech.auth
import pdi.jwt._
import org.json4s.native.Json
import org.json4s._
import org.json4s.jackson.JsonMethods._
import pdi.jwt.algorithms._
import scala.util._
object AuthBase {
type UserInfo = Map[String, Any]
case class AuthBase(
algorithm: JwtAlgorithm = JwtAlgorithm.HMD5,
secret: String = "OpenSesame",
getUserInfo: (String,String) => Option[UserInfo] = null) {
ctx =>
def withAlgorithm(algo: JwtAlgorithm): AuthBase = ctx.copy(algorithm = algo)
def withSecretKey(key: String): AuthBase = ctx.copy(secret = key)
def withUserFunc(f: (String, String) => Option[UserInfo]): AuthBase = ctx.copy(getUserInfo = f)
def authenticateToken(token: String): Option[String] =
algorithm match {
case algo: JwtAsymmetricAlgorithm =>
Jwt.isValid(token, secret, Seq((algorithm.asInstanceOf[JwtAsymmetricAlgorithm]))) match {
case true =&g