來瞧一瞧 gRPC的攔截器

[TOC]git

瞧一瞧 gRPC的攔截器

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

  • 可使用openssl作認證證書,進行認證
  • 客戶端還能夠將數據放到metadata中,服務器進行認證

但是朋友們,有沒有想過,要是每個客戶端與服務端通訊的接口都進行一次認證,那麼這是否會很是多餘呢,且每個接口的實現都要作一次認證,這真的太難受了github

咱做爲程序員,就應該要探索高效的方法來解決一些繁瑣複雜冗餘的事情。golang

今天咱們來分享一下gRPC的interceptor,即攔截器 ,相似於web框架裏的中間件。web

中間件是什麼?

是一類提供系統軟件和應用軟件之間鏈接、便於軟件各部件之間的溝通的計算機軟件,它爲軟件應用程序提供操做系統之外的服務,被形象的描述爲「軟件膠水」

直白的說,中間件便是一個系統軟件和應用軟件之間的溝通橋樑。例如他能夠記錄響應時長記錄請求和響應數據日誌服務器

中間件能夠在攔截到發送給 handler 的請求,且能夠攔截 handler 返回給客戶端的響應app

攔截器是什麼?

攔截器是gRPC生態中的中間件框架

能夠對RPC的請求和響應進行攔截處理,並且既能夠在客戶端進行攔截,也能夠對服務器端進行攔截。tcp

攔截器能作什麼?

哈哈,他能作的可多了,最終要的一點是,攔截器能夠作統一接口的認證工做,不再須要每個接口都作一次認證了,多個接口屢次訪問,只須要在統一個地方認證便可ide

這是否是大大的提升了接口的使用和認證效率了呢,同時還能夠減小代碼的冗餘度

攔截器有哪些分類呢?

根據不一樣的側重點,會有以下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的請求追蹤

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

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

相關文章
相關標籤/搜索