使用Spring Boot进行gRPC操作

本文是关于EMSLY Advent Calendar 2017的第三天文章。

我們正致力於更新所涉及的系統,並將其配置成類似於微服務的結構。目前我們正在使用Spring Boot + gRPC (+ Kotlin)來編寫服務器。

在这篇文章中,我总结了使用Spring Boot处理gRPC的Hello World的内容,并讨论了在实际应用程序开发中可能需要的身份验证和错误处理等主题。

由于当前采用的是Kotlin & Spring Boot作为服务器,而主要使用Rails作为客户端,所以示例代码可能会是Java、Kotlin或Ruby,请提前知晓。

使用Spring Boot实现gRPC

使用Spring Boot启动gRPC服务器

实际上,使用grpc-java时,您可以使用以下库在不需要进行任何特殊配置的情况下设置gRPC服务器。

日志装接入gRPC的Spring Boot启动器

    @GRpcService
    public static class GreeterService extends  GreeterGrpc.GreeterImplBase{
        @Override
        public void sayHello(GreeterOuterClass.HelloRequest request, StreamObserver<GreeterOuterClass.HelloReply> responseObserver) {
            final GreeterOuterClass.HelloReply.Builder replyBuilder = GreeterOuterClass.HelloReply.newBuilder().setMessage("Hello " + request.getName());
            responseObserver.onNext(replyBuilder.build());
            responseObserver.onCompleted();
        }
    }

只需要在gRPC的实现上添加@GRpcService注解即可。

在此状况下启动Spring Boot会…

$ ./gradlew bootRun
# 中略
2017-12-03 10:59:16.744  INFO 41958 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 9090 (http)
2017-12-03 10:59:16.747  INFO 41958 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : Starting gRPC Server ...
2017-12-03 10:59:16.875  INFO 41958 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : 'app.grpc.GreeterService' service has been registered.
2017-12-03 10:59:17.408  INFO 41958 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : gRPC Server started, listening on port 6565.
2017-12-03 10:59:17.413  INFO 41958 --- [           main] app.AppApplicationKt                     : Started AppApplicationKt in 9.786 seconds (JVM running for 10.686)

在Spring Boot服务器上,我们可以看到gRPC服务器在不同的端口上启动。

即使改变与gRPC相关的代码,编译器仍然会像修改Spring Boot代码一样,正常地重新编译,并且目前使用Spring Boot的开发体验没有任何变化。

使用Inteceptor

使用gRPC时,拦截器是关键。如其名,拦截器会拦截请求,并插入某种处理。

只要使用LogNet/grpc-spring-boot-starter,这也可以很容易地实现。

// 特定のgRPCのサービスにInterceptorを指定する
@GRpcService(interceptors = { LogInterceptor.class })
public  class GreeterService extends  GreeterGrpc.GreeterImplBase{
    // ommited
}
// 全てのサービスにこのInterceptorを指定する
@GRpcGlobalInterceptor
public  class MyInterceptor implements ServerInterceptor{
    // ommited
}
// 指定した順番でインターセプターをかませる。
@GRpcGlobalInterceptor
@Order(10)
public  class A implements ServerInterceptor{
    // will be called before B
}

@GRpcGlobalInterceptor
@Order(20)
public  class B implements ServerInterceptor{
    // will be called after A
}

通过使用这个拦截器,可以实现在应用程序中共享的处理操作。

制作应用程序时的琐事和问题。

到目前为止,我们学习了类似于”Hello World”的内容。从现在开始,我们将一起总结在实际开发应用程序时需要考虑的身份验证、错误处理等方面的内容。

确认

在gRPC的文档中,有一个关于身份验证的部分,我觉得它实际上是用于验证使用gRPC的客户端是否正确。

在进行对请求者的身份验证时,通常会使用名为Metadata的机制来附加用户的访问令牌等信息到请求中,并在服务器端进行验证,以实现身份验证。

假设您想要使用Ruby的gRPC客户端,通过使用Metadata来发送访问令牌,可以按照以下方式进行。

# 現在のプロジェクトではRubyを使っているので、サンプルはRubyで書いていますが、
# 他の言語でも同じような感じでセットできるはず。
stub = Helloworld::Greeter::Stub.new('localhost:50051', :this_channel_is_insecure)
req =  Helloworld::Greeter::HelloRequest.new(name: "suusan2go")
stub.say_hello(req, { metadata: {authorization: "76ea9743-bef9-4b1f-b116-3076ea51a1" } })

如果在服务器端处理这个问题,那就可以了,但在调用方每次验证这个东西不符合DRY原则。这种情况下使用拦截器。
以下是使用拦截器从客户端设置的认证信息中在服务器端提取的示例代码。

// 現在のプロジェクトではKotlinを使っているので、Kotlinで書いていますが、Javaでもおんなじ感じになるはず。
// エラー処理やなんやらは端折ってます。
class AuthenticationInterceptor: ServerInterceptor {
    override fun <ReqT : Any?, RespT : Any?> interceptCall(call: ServerCall<ReqT, RespT>?, headers: Metadata?,
                                                           next: ServerCallHandler<ReqT, RespT>?): ServerCall.Listener<ReqT> {
          val token: String = headers?.let {
              it.get(Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER))
          } ?: ""
          // 何か認証処理

          return next?.startCall(call, headers)!!
    }
}

我想要使用Spring Security。

说到认证,Spring Security 是春季中的指定选择。如果在使用 gRPC,显然不能直接使用 Spring Security 的功能。

我找到一个很好的例子,展示了将Spring Security和gRPC进行集成的方法。
您可以在以下链接找到该例子:https://github.com/revinate/grpc-spring-security-demo

以下是代码的摘要。
对于gRPC的每个方法,通常可以添加像在控制器上添加的@PreAuthorize(“hasRole(‘USER’)”)注解,以使其正常工作。

    @Override
    @PreAuthorize("hasRole('USER')")
    public void fibonacci(FibonacciRequest request, StreamObserver<FibonacciResponse> responseObserver) {
        if (request.getValue() < 0) {
            responseObserver.onError(Status.INVALID_ARGUMENT.withDescription("Number cannot be negative").asRuntimeException());
            return;
        }

        FibonacciResponse response = FibonacciResponse.newBuilder()
                .setValue(numberService.fibonacci(request.getValue()))
                .build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }

通常情况下,这些通过使用拦截器来自行实现,以实现Spring Security通常自动处理的功能。你可以在作者(公司?)的博客中找到详细的解释,建议你去看一看。

错误处理

如果使用gRPC,则方法的执行实际上是在远程服务器上进行的。
因此,如果在服务器端发生了任何异常,必须确保将异常信息准确传达给客户端。

比如,在Ruby客户端通过gRPC访问远程服务器时发生了异常,如果没有进行任何处理,会变成这样的情况。

# 現在のプロジェクトではRubyを使っているので、サンプルはRubyで書いていますが、
# 他の言語でも同じような感じになるはず
> stub.say_hello(req)
GRPC::Unknown: 2:

在grpc-java中,我们可以定义适当的状态码,并通过调用`call.close`来返回适当的异常和消息给客户端,这样就可以解决上述身份验证例子中的问题。

class AuthenticationInterceptor: ServerInterceptor {
    override fun <ReqT : Any?, RespT : Any?> interceptCall(call: ServerCall<ReqT, RespT>?, headers: Metadata?,
                                                           next: ServerCallHandler<ReqT, RespT>?): ServerCall.Listener<ReqT> {
        try {
            val token: String = headers?.let {
                it.get(Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER))
            } ?: ""
            // 何か認証処理
            return next?.startCall(call, headers)!!
        } catch(e: InvalidTokenException) {
            call?.close(Status.fromCode(Status.UNAUTHENTICATED.code).withDescription(e.message), Metadata())
            throw e
        }
    }
}

这样一来,客户端就能够在适当的异常类中接收到适当的错误信息。

> stub.say_hello(req, { metadata: {authorization: "76ea9743-bef9-4b1f-b116-3076ea51a1" } })
GRPC::Unauthenticated: 16:Invalid access token: 76ea9743-bef9-4b1f-b116-3076ea51a1

如果每次都继续执行此操作,那当然是冗长的。如果在gRPC服务器上想要捕获横跨的异常,可以考虑使用拦截器来实现。

关于拦截器的错误处理,nsoushi的博客对我有很大的帮助。在grpc-java的存储库中也有一些看起来很有参考价值的实现,可以去看看。

    • https://github.com/grpc/grpc-java/blob/2b1eee90e5bd7f5ad905e34f73f2040d6c9a3568/core/src/main/java/io/grpc/util/TransmitStatusRuntimeExceptionInterceptor.java

 

    http://blog.soushi.me/entry/2017/08/18/234615

总结

我总结了如何从Spring Boot处理gRPC的方法。虽然gRPC有一些难以理解的部分,但是一旦你理解了拦截器和元数据等内容,就能够基本实现应用程序所需的功能(如日志记录等)。

以下是gRPC的工作示例(虽然非常简单),希望对您有所帮助。
https://github.com/suusan2go/nuxtjs-auth-with-spring/tree/master/spring-backend

请参考以下资料

平日インプット週末アウトプットぶろぐ

grpc-javaについての知見を沢山公開されていて、とても参考になりました

Securing Java gRPC services with Spring Security

Spring Security + gRPC について実践的な内容が詳しく記載されています

gRPC and REST with gRPC in practice

gRPCについてざっくり理解するにあたり、大変参考になりました

广告
将在 10 秒后关闭
bannerAds