来瞧一瞧 gRPC的拦截器

[TOC]

瞧一瞧 gRPC的拦截器

拦截器.jpg
image

上一次说到gRPC的认证总共有4种,其中介绍了常用且重要的2种

  • 可以使用openssl做认证证书,进行认证
  • 客户端还可以将数据放到metadata中,服务器进行认证

可是朋友们,有没有想过,要是每一个客户端与服务端通信的接口都进行一次认证,那么这是否会非常多余呢,且每一个接口的实现都要做一次认证,这真的太难受了

咱作为程序员,就应该要探索高效的方法来解决一些繁琐复杂冗余的事情。

今天我们来分享一下gRPC的interceptor,即拦截器 ,类似于web框架里的中间件。

中间件是什么?

是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的计算机软件,它为软件应用程序提供操作系统以外的服务,被形象的描述为“软件胶水”

直白的说,中间件即是一个系统软件和应用软件之间的沟通桥梁。例如他可以记录响应时长记录请求和响应数据日志

中间件可以在拦截到发送给 handler 的请求,且可以拦截 handler 返回给客户端的响应

拦截器是什么?

拦截器是gRPC生态中的中间件

可以对RPC的请求和响应进行拦截处理,而且既可以在客户端进行拦截,也可以对服务器端进行拦截。

image

拦截器能做什么?

哈哈,他能做的可多了,最终要的一点是,拦截器可以做统一接口的认证工作,再也不需要每一个接口都做一次认证了,多个接口多次访问,只需要在统一个地方认证即可

这是不是大大的提高了接口的使用和认证效率了呢,同时还可以减少代码的冗余度

拦截器有哪些分类呢?

根据不同的侧重点,会有如下2种分类:

image

侧重点不同,分类的拦截器也不同,不过使用的方式都是大同小异的。

如何使用拦截器?

服务端会用到的方法

image

UnaryServerInterceptor提供了一个钩子来拦截服务器上单一RPC的执行,拦截器负责调用处理程序来完成RPC

其中参数中的UnaryHandler定义了由UnaryServerInterceptor调用的处理程序

客户端会用到的方法

image
type UnaryClientInterceptor func(
    ctx context.Context,        // 上下文
    method string,              // RPC的名字,例如此处我们使用的是gRPC
    req, reply interface{},     // 对应的请求和响应消息
    cc *ClientConn,             // cc是调用RPC的ClientConn
    invoker UnaryInvoker,       // invoker是完成RPC的处理程序,主要是调用它是拦截器
    opts ...CallOption) error   // opts包含所有适用的调用选项,包括来自ClientConn的默认值以及每个调用选项

整体案例代码结构

代码结构与上2篇分享到的结构一致,本次拦截器,是统一做认证,把认证的地方统一放在同一个位置,而不是分散到每一个接口

若需要具体的proto源码,可以查看我的上一期文章,如下为代码结构图示

image

开始书写案例

  • 在原有代码基础上加入interceptor的功能,目前案例中注册一个拦截器
  • gRPC + openssl + token + interceptor

server.go

  • 主要加入UnaryServerInterceptor来对拦截器的应用
package main

import (
   "fmt"
   "google.golang.org/grpc/codes"
   "google.golang.org/grpc/metadata"
   "log"
   "net"

   pb "myserver/protoc/hi"

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入grpc认证包
)

const (
   // Address gRPC服务地址
   Address = "127.0.0.1:9999"
)

// 定义helloService并实现约定的接口
type HiService struct{}

// HiService Hello服务
var HiSer = HiService{}

// SayHello 实现Hello服务接口
func (h HiService) SayHi(ctx context.Context, in *pb.HiRequest) (*pb.HiResponse, error) {

   // 解析metada中的信息并验证
   md, ok := metadata.FromIncomingContext(ctx)
   if !ok {
      return nil, grpc.Errorf(codes.Unauthenticated, "no token ")
   }

   var (
      appId  string
      appKey string
   )

   // md 是一个 map[string][]string 类型的
   if val, ok := md["appid"]; ok {
      appId = val[0]
   }

   if val, ok := md["appkey"]; ok {
      appKey = val[0]
   }

   if appId != "myappid" || appKey != "mykey" {
      return nil, grpc.Errorf(codes.Unauthenticated, "token invalide: appid=%s, appkey=%s", appId, appKey)
   }

   resp := new(pb.HiResponse)
   resp.Message = fmt.Sprintf("Hi %s.", in.Name)

   return resp, nil
}

// 认证token
func myAuth(ctx context.Context) error {
   md, ok := metadata.FromIncomingContext(ctx)
   if !ok {
      return grpc.Errorf(codes.Unauthenticated, "no token ")
   }

   log.Println("myAuth ...")

   var (
      appId  string
      appKey string
   )

   // md 是一个 map[string][]string 类型的
   if val, ok := md["appid"]; ok {
      appId = val[0]
   }

   if val, ok := md["appkey"]; ok {
      appKey = val[0]
   }

   if appId != "myappid" || appKey != "mykey" {
      return grpc.Errorf(codes.Unauthenticated, "token invalide: appid=%s, appkey=%s", appId, appKey)
   }

   return nil
}

// interceptor 拦截器
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
   // 进行认证
   log.Println("interceptor...")
   err := myAuth(ctx)
   if err != nil {
      return nil, err
   }

   // 继续处理请求
   return handler(ctx, req)
}

func main() {
   log.SetFlags(log.Ltime | log.Llongfile)

   listen, err := net.Listen("tcp", Address)
   if err != nil {
      log.Panicf("Failed to listen: %v", err)
   }

   var opts []grpc.ServerOption

   // TLS认证
   creds, err := credentials.NewServerTLSFromFile("./keys/server.pem", "./keys/server.key")
   if err != nil {
      log.Panicf("Failed to generate credentials %v", err)
   }

   opts = append(opts, grpc.Creds(creds))

   // 注册一个拦截器
   opts = append(opts, grpc.UnaryInterceptor(interceptor))

   // 实例化grpc Server, 并开启TLS认证,其中还有拦截器
   s := grpc.NewServer(opts...)

   // 注册HelloService
   pb.RegisterHiServer(s, HiSer)

   log.Println("Listen on " + Address + " with TLS and interceptor")

   s.Serve(listen)
}

client.go

  • 主要加入UnaryClientInterceptor来对拦截器的应用
package main

import (
   "log"
   pb "myclient/protoc/hi" // 引入proto包
   "time"

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入grpc认证包
   "google.golang.org/grpc/grpclog"
)

const (
   // Address gRPC服务地址
   Address = "127.0.0.1:9999"
)

var IsTls = true

// myCredential 自定义认证
type myCredential struct{}

// GetRequestMetadata 实现自定义认证接口
func (c myCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
   return map[string]string{
      "appid":  "myappid",
      "appkey": "mykey",
   }, nil
}

// RequireTransportSecurity 自定义认证是否开启TLS
func (c myCredential) RequireTransportSecurity() bool {
   return IsTls
}

// 客户端拦截器
func Clientinterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
   start := time.Now()
   err := invoker(ctx, method, req, reply, cc, opts...)
   log.Printf("method == %s ; req == %v ; rep == %v ; duration == %s ; error == %v\n", method, req, reply, time.Since(start), err)
   return err
}

func main() {
   log.SetFlags(log.Ltime | log.Llongfile)
   // TLS连接  记得把xxx改成你写的服务器地址

   var err error
   var opts []grpc.DialOption

   if IsTls {
      //打开tls 走tls认证
      creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "www.eline.com")
      if err != nil {
         log.Panicf("Failed to create TLS mycredentials %v", err)
      }
      opts = append(opts, grpc.WithTransportCredentials(creds))
   } else {
      opts = append(opts, grpc.WithInsecure())
   }

   // 自定义认证,new(myCredential 的时候,由于我们实现了上述2个接口,因此new的时候,程序会执行我们实现的接口
   opts = append(opts, grpc.WithPerRPCCredentials(new(myCredential)))

   // 加上拦截器
   opts = append(opts, grpc.WithUnaryInterceptor(Clientinterceptor))

   conn, err := grpc.Dial(Address, opts...)
   if err != nil {
      grpclog.Fatalln(err)
   }

   defer conn.Close()

   // 初始化客户端
   c := pb.NewHiClient(conn)

   // 调用方法
   req := &pb.HiRequest{Name: "gRPC"}
   res, err := c.SayHi(context.Background(), req)
   if err != nil {
      log.Panicln(err)
   }
   log.Println(res.Message)

   // 故意再调用一次
   res, err = c.SayHi(context.Background(), req)
   if err != nil {
      log.Panicln(err)
   }

   log.Println(res.Message)
}

实际效果展示

image
image

注意,服务器只能配置一个 UnaryInterceptorStreamClientInterceptor,否则会报错,客户端也是,虽然不会报错,但是只有最后一个才起作用。 如果你想配置多个,可以使用拦截器链,如go-grpc-middleware,或者自己实现。

  • 服务端的拦截器
    • UnaryServerInterceptor -- 单向调用的拦截器
    • StreamServerInterceptor -- stream调用的拦截器
  • 客户端的拦截器
    • UnaryClientInterceptor
    • StreamClientInterceptor

上述拦截器无论是单向调用的拦截器 还是 stream调用的拦截器 用法都大同小异

// 服务端
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

// 客户端
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

type StreamClientInterceptor func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

最后分享社区内用到的拦截器(还应该有更多...)

最后与大家分享几个社区内用到的拦截器

用于身份验证拦截器

interceptor链式功能的库,可以将单向的或者流式的拦截器组合

为上下文增加Tag map对象

日志框架

可以为客户端增加重试的功能

好了,本次就到这里,下一次分享 gRPC的请求追踪

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容

  • gRPC 作为一套独立的 RPC 框架,像拦截器这种功能当然也是不可或缺的,框架自带的拦截器更多是基于框架本身出发...
    BeckJin阅读 2,658评论 0 3
  • 在微服务架构中,调用链是漫长而复杂的,要了解其中的每个环节及其性能,你需要全链路跟踪。它的原理很简单,你可以在每个...
    51reboot阅读 962评论 0 1
  • 文档不全仍需要补充,具体可以参考 Grpc.Core.Api/Interceptors .Net 中的 Grpc ...
    ZeroingX阅读 4,560评论 1 2
  • 在微服务架构中,调用链是漫长而复杂的,要了解其中的每个环节及其性能,你需要全链路跟踪。 它的原理很简单,你可以在每...
    倚天码农阅读 1,045评论 0 0
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,931评论 2 7