上一次說到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)
}
實際效果展示
注意,伺服器只能配置一個 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)
最後分享社群內用到的攔截器(還應該有更多…)
最後與大家分享幾個社群內用到的攔截器
用於身份驗證攔截器
- grpc_auth: github.com/grpc-ecosystem/go-grpc-...
interceptor鏈式功能的庫,可以將單向的或者流式的攔截器組合
- grpc-multi-interceptor: github.com/kazegusuri/grpc-multi-i...
- go-grpc-middleware: github.com/grpc-ecosystem/go-grpc-...
為上下文增加Tag
map物件
- grpc_ctxtags: github.com/grpc-ecosystem/go-grpc-...
日誌框架
可以為客戶端增加重試的功能
- grpc_retry: github.com/grpc-ecosystem/go-grpc-...
好了,本次就到這裡,下一次分享 gRPC的請求追蹤,
技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。
我是小魔童哪吒,歡迎點贊關注收藏,下次見~
本作品採用《CC 協議》,轉載必須註明作者和本文連結