來瞧一瞧 gRPC的攔截器

小魔童哪吒發表於2021-05-30
[TOC]

上一次說到gRPC的認證總共有4種,其中介紹了常用且重要的2種

  • 可以使用openssl做認證證照,進行認證
  • 客戶端還可以將資料放到metadata中,伺服器進行認證

可是朋友們,有沒有想過,要是每一個客戶端與服務端通訊的介面都進行一次認證,那麼這是否會非常多餘呢,且每一個介面的實現都要做一次認證,這真的太難受了

我們作為程式設計師,就應該要探索高效的方法來解決一些繁瑣複雜冗餘的事情。

今天我們來分享一下gRPC的interceptor,即攔截器 ,類似於web框架裡的中介軟體。

中介軟體是什麼?

是一類提供系統軟體和應用軟體之間連線、便於軟體各部件之間的溝通的計算機軟體,它為軟體應用程式提供作業系統以外的服務,被形象的描述為“軟體膠水”

直白的說,中介軟體即是一個系統軟體和應用軟體之間的溝通橋樑。例如他可以記錄響應時長記錄請求和響應資料日誌

中介軟體可以在攔截到傳送給 handler 的請求,且可以攔截 handler 返回給客戶端的響應

攔截器是什麼?

攔截器是gRPC生態中的中介軟體

可以對RPC的請求和響應進行攔截處理,而且既可以在客戶端進行攔截,也可以對伺服器端進行攔截。

攔截器能做什麼?

哈哈,他能做的可多了,最終要的一點是,攔截器可以做統一介面的認證工作,再也不需要每一個介面都做一次認證了,多個介面多次訪問,只需要在統一個地方認證即可

這是不是大大的提高了介面的使用和認證效率了呢,同時還可以減少程式碼的冗餘度

攔截器有哪些分類呢?

根據不同的側重點,會有如下2種分類:

側重點不同,分類的攔截器也不同,不過使用的方式都是大同小異的。

如何使用攔截器?

服務端會用到的方法

UnaryServerInterceptor提供了一個鉤子來攔截伺服器上單一RPC的執行,攔截器負責呼叫處理程式來完成RPC

其中引數中的UnaryHandler定義了由UnaryServerInterceptor呼叫的處理程式

客戶端會用到的方法

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原始碼,可以檢視我的上一期文章,如下為程式碼結構圖示

開始書寫案例

  • 在原有程式碼基礎上加入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)
}

實際效果展示

注意,伺服器只能配置一個 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的請求追蹤

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章