Grpc 整合 Nacos SpringBoot 日常使用(Java版本)包括 Jwt 认证

这里写目录标题

    • 本文项目地址
    • 前言
    • Grpc 传输介质介绍
    • Protobuf 语法介绍
    • Protobuf 插件指定生成文件目录(Maven插件)
    • Grpc 服务端配置(YML文件)
    • Grpc 服务端 Api 接口实现
    • Grpc 通信客户端配置(YML文件)
    • Grpc 客户端服务调用(阻塞式)
    • Grpc 客户端服务调用(服务端单向流)
    • Grpc 服务端非阻塞模式(服务端单向流)
    • Grpc 双向流
    • 客户端 Grpc 拦截器日志记录、jwt 请求头统一添加
    • 服务端 Grpc 拦截器权限校验
    • 附录(本文所用到的Grpc插件、proto文件、maven依赖)

本文项目地址

感兴趣的小伙伴可以支持一下点个 Star。
项目地址:https://gitcode.net/qq_42875345/grpcdemo

前言

最近感到有点子迷茫,天天写业务代码有点麻木,趁着有点空闲时间去了解了下 Grpc 这个框架,一方面是听说他很火,支持多种语言。另一方面也是为了将来可能需要用到他,未雨绸缪一下,当然了本文只是基于使用上来带大家入门 Grpc,应付基本的日常开发足够了,后续有时间给大家分析一波源码,帮助大家更好的理解 net.devh.boot.grpc 这个包里面关于自动装配、Grpc服务是如何注册、@GrpcService以及内置注解是如何生效的。

Grpc 传输介质介绍

Grpc 是基于 Protobuf 序列化传输的,为啥用 Protobuf 那就是传输效率高,Grpc 的基础代码是根据我们编写的 Protobuf 文件,通过 Protobuf 插件逆向生成的,有点类似于 Mybatis 的逆向生成,所以在学习 Grpc 前,我们要先了解下 Protobuf 的基础语法。

Protobuf 语法介绍

就着本文使用到的 proto 文件来介绍吧,如下几个参数类似于 java 中的 import、papackage 这些关键字,对应的作用已用注释说明。

//是否生成多个文件
option java_multiple_files = false;
//生成的代码放在所指定的包下面
option java_package = "com.zzh.grpcapi.rpc";
//生成文件名称
option java_outer_classname = "UserServiceProto";

当我们通过 Protobuf 插件编译完成后,项目对应的 com.zzh.grpcapi.rpc 目录下就会出现如下图的这些文件。我们日后的开发用的就是这些逆向生成的代码。


但是生成文件到指定目录需要一个前提就是我们做过相关的配置,也就是引入我们的 Protobuf 插件。

Protobuf 插件指定生成文件目录(Maven插件)

在 bulid 下面的 plugins 标签中添加一个 如下的一个 plugin 节点即可,plugin 代码在文章末尾的附录里有。

插件代码一般都是拿来即用的,唯一需要大家做出一点改动的地方就是如下这俩个节点,outputDirectory 改成你项目中,对应的生成文件需要放的位置,clearOutputDirectory 节点用 false 就行,开发中类中的方法会出现增增减减的情况,用 false 表示对应的逆向文件只更新我们 proto 文件中更新的内容,其他内容不变。而用 true 表示,先删除目录下的所有文件,然后重新生成我们的代码。

<!--指定生成文件目录位置-->
<outputDirectory>${basedir}/src/main/java</outputDirectory>
<!--false:追加,true:清除指定目录下的文件然后覆盖-->
<clearOutputDirectory>false</clearOutputDirectory>

plugin 配置都加好了后 ,依次点击 protobuf: compile , protobuf: compile-custom 即可。(我们的 proto 文件夹必须是在 main 文件下面,不然 proto 文件找不到

Grpc 整合 Nacos SpringBoot 日常使用(Java版本)包括 Jwt 认证_第1张图片

Grpc 服务端配置(YML文件)

除了需要配置一下 Grpc 服务的端口号,其他配置均为 Nacos 的大众配置,为什么 Grpc 需要额外的配置一下端口号呢?原因也很简单 Grpc 是基于 Netty 的二次开发,不管是 Grpc 还是 Netty 最终实现远程通信都离不开 Scoket 这个东西,而 server port 指定的是我们 tomcat 的端口号,grpc server port 指定的则是 Scoket 的端口号。这俩个端口号是俩码事(后续推出一遍源码博客帮助大家了解吧~)

server:
  port: 8089
grpc:
  server:
    port: 8090
spring:
  application:
    name: grpcservice
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848

配置好了后记得引入我们的 api 模块,加入到 dependencies 中即可。

<dependency>
    <groupId>com.zzhgroupId>
    <artifactId>grpcapiartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <scope>compilescope>
dependency>

Grpc 服务端 Api 接口实现

我们实现 UserServiceGrpc 中的以 Base 结尾的那个类即可,这个类也是大家业务开发中需要实现的类,然后客户端调用的也是我们实现类中的方法逻辑。指的注意的是:Grpc 的返回数据、异常返回和我们 Java 中接口的返回有点不太一样。

  1. Grpc 返回数据:通过 onNext 方法返回客户端数据。且 onCompleted 方法必须调用,代表此次 Grpc 服务通信结束。
  2. Grpc 异常返回: 通过 ReponseObserver.onError() 方法返回。

这些代码都是固定的,为了加深理解,读者自行去测试看效果将会事半功倍。本文只阐述最基本的代码通用模版。常用的开发中也就是这些个东西了。最后我们用 @GrpcService 注解标注此类为 Grpc 服务类即可。

Grpc 通信客户端配置(YML文件)

里面就是一些 Nacos 注册的配置还有 Grpc 探测服务的配置,值得一提的是,如项目整合了 Nacos,那么 address: static://127.0.0.1:8090 下文配置中的这一行代码将无需配置,原因很简单,客户端和服务端注册在同一个 Nacos 里面,当客户端需要用到服务端时,根据服务名称寻找即可找到对应的 Grpc 服务,但是当项目没整合 Nacos 时,则需要我们手动配置 Grpc 服务地址。

server:
  port: 8088
grpc:
  client:
    grpc-service-test:
      #address: static://127.0.0.1:8090 #整合nacos后无需设置,grrpc服务端地址,根据服务名寻找对应的服务
      negotiation-type: plaintext
      enableKeepAlive: true
      keepAliveWithoutCalls: true
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
  application:
    name: grpcclient

注意我们的 grpc-service-test 不是乱配的,需要和 Nacos 中的服务名对应。
在这里插入图片描述

Grpc 客户端服务调用(阻塞式)

客户端都配置好了之后,我们利用 @GrpcClient 注解,注入我们的 Grpc 服务端实现类,即可在客户端完成调用。日常开发用下图中的阻塞客户端即可,意思就是这次 Grpc 远程调用必须等待服务端处理好请求,客户端收到请求后接着处理客户端后续的逻辑(日常开发最常用的也是这种模式),为阻塞模式。那有的读者就会问了,有没有非阻塞的调用呀,有的请看下文。
Grpc 整合 Nacos SpringBoot 日常使用(Java版本)包括 Jwt 认证_第2张图片

Grpc 客户端服务调用(服务端单向流)

大致代码和阻塞式差不多,但是区别在于 Grpc 的调用是靠 UserServiceStub 完成的,也就是非阻塞客户端(使用场景:聊天室、股票代码、实时天气),非阻塞模式的调用,在不阻塞客户端逻辑的情况下,先出给接口响应,后续的 Grpc 推送过来的消息采用监听的模式接收,整个过程是异步的。因此 Grpc 贴心的为我们提供了 onNext、onError、onCompleted 这三大方法分别代表如下意思。

  1. onNext:Grpc 服务端每推送一条消息给客户端,onNext 方法里面都会同步监听到这条数据
  2. onError:Grpc 服务端发生异常时,onError 方法里面会同步监听到错误信息
  3. onCompleted: Grpc 服务端调用 onCompleted 方法时,客户端同步触发 onCompleted 方法。

Grpc 整合 Nacos SpringBoot 日常使用(Java版本)包括 Jwt 认证_第3张图片
当然非阻塞式方法需要对应的 proto 文件支持,需要哪边(客户端、服务端)具备主动推送消息的能力就在对应的参数(请求参数、返回参数)前面添加 stream 。

rpc loginStream (LoginVo) returns (stream LoginDto) {}

Grpc 服务端非阻塞模式(服务端单向流)

客户端改造好了后,服务端用 for 循环每隔 2 秒,依次调用 onNext 方法即可,客户端的 onNext 监听方法里面也会每隔 2 秒收到来自 Grpc 服务端的消息。

Grpc 整合 Nacos SpringBoot 日常使用(Java版本)包括 Jwt 认证_第4张图片

演示效果:每隔 2 秒收到来自服务端推送过来的消息

Grpc 整合 Nacos SpringBoot 日常使用(Java版本)包括 Jwt 认证_第5张图片

Grpc 双向流

Grpc 还有一个功能就是可以实现,客户端与服务端互推消息。 有点类似于 WebScoket 。使用起来也很简单,客户端直接调用双向流的方法,里面还是那三个监听方法,监听 Grpc 服务端推送过来的消息,同时客户端利用调用方法返回的 StreamObserver 可以去主动推送给 Grpc 服务端。完成了客户端的主动推送。

Grpc 整合 Nacos SpringBoot 日常使用(Java版本)包括 Jwt 认证_第6张图片

而服务端也是利用 StreamObserver 完成去监听客户端发来的消息和推送给客户端消息。下图的代码我是客户端发过来一条,服务端就处理一条推送回给客户端的,各位可以根据自己的业务逻辑来。

Grpc 整合 Nacos SpringBoot 日常使用(Java版本)包括 Jwt 认证_第7张图片

到此 Grpc 几种常用的模式就介绍完了,接下来在说一下 Grpc 中的拦截器吧,这个大家也有可能会用到。

客户端 Grpc 拦截器日志记录、jwt 请求头统一添加

有的时候为了保证 Grpc 服务的安全,一般都需要做认证才能进行调用,利用客户端拦截器我们可以对 Grpc 调用进行日志记录,也可以对所有的 Grpc 调用添加统一的请求头,做 Jwt 校验啥的。我们只需实现 ClientInterceptor 接口,然后在所在类上面加上 @GrpcGlobalClientInterceptor 注解即可。标注这个是全局客户端拦截器。下面的代码我会在所有 Grpc 请求调用前为当期请求设置一个 jwt 请求头,这个 jwt 是服务端给我们颁发的,如果客户端没有检测到 jwt 的存在会携带 ak、sk 参数主动去请求服务端的 jwt 获取接口,完成后存储于客户端,可能有人会说了,jwt 不是无状态的吗?你这样不就成了有状态的了。答:jwt 存储在客户端,相当于我们的前端页面,对于服务端依旧是无状态的。

/**
 * 全局设置 jwt 请求头
 */
@Slf4j
@GrpcGlobalClientInterceptor
public class ClientInterceptor implements io.grpc.ClientInterceptor {
    @Value("${grpc.auth.ak}")
    private String ak;
    @Value("${grpc.auth.sk}")
    private String sk;
    @Value("${grpc.auth.host}")
    private String host;
    @Value("${grpc.auth.port}")
    private int port;
    @Value("${grpc.auth.name}")
    private String serviceName;
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
        CallOptions myCallOptions = callOptions
                //.withDeadlineAfter(2, TimeUnit.SECONDS) //设置超时
                .withCallCredentials(new CallCredentials() {
                    @Override
                    public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
                        Metadata metadata = new Metadata();
                        Object token = redisTemplate.opsForValue().get(serviceName);
                        String jwt = "";
                        if (null == token) {
                            //根据服务名进行寻找服务,利用 ak、sk 自动登录,将 token 存储于 redis
                            ManagedChannel managedChannel = ManagedChannelBuilder.forTarget(serviceName).usePlaintext().build();
                            UserServiceGrpc.UserServiceBlockingStub userServiceBlockingStub = UserServiceGrpc.newBlockingStub(managedChannel);
                            UserServiceProto.LoginDto login = userServiceBlockingStub
                                    .login(UserServiceProto.LoginVo.newBuilder()
                                            .setName(ak)
                                            .setPassword(sk)
                                            .build());
                            jwt = login.getToken();
                            redisTemplate.opsForValue().set(serviceName, jwt);
                        } else {
                            jwt = String.valueOf(token);
                        }
                        //设置 jwt 请求头
                        metadata.put(Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), "Bearer " + jwt);
                        applier.apply(metadata);
                    }
                    @Override
                    public void thisUsesUnstableApi() {
                    }
                });
        return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, myCallOptions)) {
            @Override
            public void sendMessage(ReqT message) {
                //日志记录
                log.info("ClientInterceptor#interceptCall#SimpleForwardingClientCall#sendMessage## request method: {} , param: {} ", method.getFullMethodName(), message.toString());
                super.sendMessage(message);
            }
        };
    }
}

但是上面的代码有一个问题,如果当前项目需要调用多个不同的 Grpc 服务,且每个 Grpc 服务都有各自的认证方式的话,那么这种全局添加请求头这种方式就不太适合了,那么这个时候我们根据调用的 Grpc 服务名(通过 requestInfo.getAuthority()获取),去请求对应的认证接口,然后把拿到的 token 放到请求头或者是哪,这个属于业务问题了,读者根据自己的需求来即可。

服务端 Grpc 拦截器权限校验

大致逻辑就是如果没带 jwt 请求头提示客户端没权限访问,带了 jwt 请求头去校验 jwt 是否合法,合法了放行。放行和阻塞的代码都是固定的,大家根据自己需要设置对应的 Status、还有上下文参数即可。

@GrpcGlobalServerInterceptor
public class AuthInterceptor implements ServerInterceptor {
    private JwtParser parser = Jwts.parser().setSigningKey(AuthConstant.JWT_KEY);
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata, ServerCallHandler<ReqT, RespT> serverCallHandler) {
        String authorization = metadata.get(Metadata.Key.of(AuthConstant.AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER));
        Status status = Status.OK;
        //简单模拟了一下白名单,实际开发中放配置文件
        if ("UserService/login".equals(serverCall.getMethodDescriptor().getFullMethodName()))
            //放行
            return Contexts.interceptCall(Context.current(), serverCall, metadata, serverCallHandler);
        if (authorization == null) {
            status = Status.UNAUTHENTICATED.withDescription("miss authentication token");
        } else if (!authorization.startsWith(AuthConstant.AUTH_TOKEN_TYPE)) {
            status = Status.UNAUTHENTICATED.withDescription("unknown token type");
        } else {
            Jws<Claims> claims = null;
            String token = authorization.substring(AuthConstant.AUTH_TOKEN_TYPE.length()).trim();
            try {
                claims = parser.parseClaimsJws(token);
            } catch (JwtException e) {
                status = Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e);
            }
            if (claims != null) {
                //设置全局上下文属性,下游通过 AuthConstant.AUTH_CLIENT_ID.get(Context.current()) 获取设置的值
                Context ctx = Context.current().withValue(AuthConstant.AUTH_CLIENT_ID, claims.getBody().getSubject());
                //放行
                return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler);
            }
        }
        //阻塞
        serverCall.close(status, new Metadata());
        return new ServerCall.Listener<ReqT>() {
        };
    }
}

public interface AuthConstant {
    SecretKey JWT_KEY = Keys.hmacShaKeyFor("zzhhaoshuaizzhhaoshuaizzhhaoshuaizzhhaoshuai".getBytes());
    Context.Key<String> AUTH_CLIENT_ID = Context.key("userId");
    String AUTH_HEADER = "Authorization";
    String AUTH_TOKEN_TYPE = "Bearer";
}

到此你已经可以利用 Grpc 进行日常业务开发了,剩下的就是搬砖操作了~~~~~~~~~~~~

附录(本文所用到的Grpc插件、proto文件、maven依赖)

本文使用到的 Grpc 插件

   <extensions>
        
        <extension>
            <groupId>kr.motd.mavengroupId>
            <artifactId>os-maven-pluginartifactId>
            <version>1.6.2version>
        extension>
    extensions>
    <plugins>
        
        <plugin>
            <groupId>org.xolstice.maven.pluginsgroupId>
            <artifactId>protobuf-maven-pluginartifactId>
            <version>0.6.1version>
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}protocArtifact>
                <pluginId>grpc-javapluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}pluginArtifact>
                <outputDirectory>${basedir}/src/main/javaoutputDirectory>
                <clearOutputDirectory>falseclearOutputDirectory>
            configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compilegoal>
                        <goal>compile-customgoal>
                    goals>
                execution>
            executions>
        plugin>

本文用到的 Nacos、Grpc、Jwt Maven 依赖

    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <maven.compiler.source>1.8maven.compiler.source>
        <maven.compiler.target>1.8maven.compiler.target>
        <grpc.version>1.44.0grpc.version>
        <protobuf.version>3.19.2protobuf.version>
        <protoc.version>3.19.2protoc.version>
        <gson.version>2.8.9gson.version>
        <spring-boot.version>2.3.9.RELEASEspring-boot.version>

    properties>

    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-dependenciesartifactId>
                <version>${spring-boot.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
            
            <dependency>
                <groupId>io.grpcgroupId>
                <artifactId>grpc-bomartifactId>
                <version>${grpc.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
            
            <dependency>
                <groupId>com.google.protobufgroupId>
                <artifactId>protobuf-java-utilartifactId>
                <version>${protobuf.version}version>
            dependency>
            
            <dependency>
                <groupId>com.google.code.gsongroupId>
                <artifactId>gsonartifactId>
                <version>${gson.version}version>
            dependency>
        dependencies>



    dependencyManagement>


    <dependencies>

        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-alibaba-nacos-configartifactId>
            <version>2.2.0.RELEASEversion>
        dependency>
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
            <version>2.2.5.RELEASEversion>
        dependency>

        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwt-apiartifactId>
            <version>0.11.5version>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwt-implartifactId>
            <version>0.11.5version>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwt-jacksonartifactId>
            <version>0.11.5version>
            <scope>runtimescope>
        dependency>
        
        <dependency>
            <groupId>io.grpcgroupId>
            <artifactId>grpc-netty-shadedartifactId>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>io.grpcgroupId>
            <artifactId>grpc-protobufartifactId>
        dependency>
        <dependency>
            <groupId>io.grpcgroupId>
            <artifactId>grpc-stubartifactId>
        dependency>

        
        <dependency>
            <groupId>com.google.protobufgroupId>
            <artifactId>protobuf-java-utilartifactId>
        dependency>
        
        <dependency>
            <groupId>com.google.code.gsongroupId>
            <artifactId>gsonartifactId>
        dependency>
        <dependency>
            <groupId>net.devhgroupId>
            <artifactId>grpc-spring-boot-starterartifactId>
            <version>2.14.0.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.8version>
            <scope>providedscope>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>com.zzhgroupId>
            <artifactId>grpcapiartifactId>
            <version>0.0.1-SNAPSHOTversion>
            <scope>compilescope>
        dependency>
    dependencies>

本文测试用到的 proto 文件

syntax = "proto3";
//是否生成多个文件
option java_multiple_files = false;
//生成的代码放在所指定的包下面
option java_package = "com.zzh.grpcapi.rpc";
//生成文件名称
option java_outer_classname = "UserServiceProto";
service UserService {
  rpc login (LoginVo) returns (LoginDto) {}
  rpc loginStream (LoginVo) returns (stream LoginDto) {}
  rpc loginDoubleStream (stream LoginVo) returns (stream LoginDto) {}
}

message LoginVo {
  string name = 1;
  string password = 2;

}

message LoginDto {
  string name = 1;
  repeated string ids = 3;
  string password = 2;
  string token=4;
}

你可能感兴趣的:(grpc,java,spring,boot,grpc,jwt)