[TOC]git
上一次說到gRPC的認證總共有4種,其中介紹了經常使用且重要的2種:程序員
但是朋友們,有沒有想過,要是每個客戶端與服務端通訊的接口都進行一次認證,那麼這是否會很是多餘呢,且每個接口的實現都要作一次認證,這真的太難受了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源碼,能夠查看個人上一期文章,以下爲代碼結構圖示
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) }
實際效果展現
注意,服務器只能配置一個 UnaryInterceptor
和StreamClientInterceptor
,不然會報錯,客戶端也是,雖然不會報錯,可是隻有最後一個才起做用。 若是你想配置多個,可使用攔截器鏈,如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的請求追蹤,
技術是開放的,咱們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。
我是小魔童哪吒,歡迎點贊關注收藏,下次見~