[TOC]
瞧一瞧 gRPC的攔截器


上一次說(shuō)到gRPC的認(rèn)證總共有4種,其中介紹了常用且重要的2種:
- 可以使用openssl做認(rèn)證證書,進(jìn)行認(rèn)證
- 客戶端還可以將數(shù)據(jù)放到metadata中,服務(wù)器進(jìn)行認(rèn)證
可是朋友們,有沒(méi)有想過(guò),要是每一個(gè)客戶端與服務(wù)端通信的接口都進(jìn)行一次認(rèn)證,那么這是否會(huì)非常多余呢,且每一個(gè)接口的實(shí)現(xiàn)都要做一次認(rèn)證,這真的太難受了
咱作為程序員,就應(yīng)該要探索高效的方法來(lái)解決一些繁瑣復(fù)雜冗余的事情。
今天我們來(lái)分享一下gRPC的interceptor,即攔截器 ,類似于web框架里的中間件。
中間件是什么?
是一類提供系統(tǒng)軟件和應(yīng)用軟件之間連接、便于軟件各部件之間的溝通的計(jì)算機(jī)軟件,它為軟件應(yīng)用程序提供操作系統(tǒng)以外的服務(wù),被形象的描述為“軟件膠水”
直白的說(shuō),中間件即是一個(gè)系統(tǒng)軟件和應(yīng)用軟件之間的溝通橋梁。例如他可以記錄響應(yīng)時(shí)長(zhǎng)、記錄請(qǐng)求和響應(yīng)數(shù)據(jù)日志等
中間件可以在攔截到發(fā)送給 handler 的請(qǐng)求,且可以攔截 handler 返回給客戶端的響應(yīng)
攔截器是什么?
攔截器是gRPC生態(tài)中的中間件
可以對(duì)RPC的請(qǐng)求和響應(yīng)進(jìn)行攔截處理,而且既可以在客戶端進(jìn)行攔截,也可以對(duì)服務(wù)器端進(jìn)行攔截。

攔截器能做什么?
哈哈,他能做的可多了,最終要的一點(diǎn)是,攔截器可以做統(tǒng)一接口的認(rèn)證工作,再也不需要每一個(gè)接口都做一次認(rèn)證了,多個(gè)接口多次訪問(wèn),只需要在統(tǒng)一個(gè)地方認(rèn)證即可
這是不是大大的提高了接口的使用和認(rèn)證效率了呢,同時(shí)還可以減少代碼的冗余度
攔截器有哪些分類呢?
根據(jù)不同的側(cè)重點(diǎn),會(huì)有如下2種分類:

側(cè)重點(diǎn)不同,分類的攔截器也不同,不過(guò)使用的方式都是大同小異的。
如何使用攔截器?
服務(wù)端會(huì)用到的方法

UnaryServerInterceptor提供了一個(gè)鉤子來(lái)攔截服務(wù)器上單一RPC的執(zhí)行,攔截器負(fù)責(zé)調(diào)用處理程序來(lái)完成RPC
其中參數(shù)中的UnaryHandler定義了由UnaryServerInterceptor調(diào)用的處理程序
客戶端會(huì)用到的方法

type UnaryClientInterceptor func(
ctx context.Context, // 上下文
method string, // RPC的名字,例如此處我們使用的是gRPC
req, reply interface{}, // 對(duì)應(yīng)的請(qǐng)求和響應(yīng)消息
cc *ClientConn, // cc是調(diào)用RPC的ClientConn
invoker UnaryInvoker, // invoker是完成RPC的處理程序,主要是調(diào)用它是攔截器
opts ...CallOption) error // opts包含所有適用的調(diào)用選項(xiàng),包括來(lái)自ClientConn的默認(rèn)值以及每個(gè)調(diào)用選項(xiàng)
整體案例代碼結(jié)構(gòu)
代碼結(jié)構(gòu)與上2篇分享到的結(jié)構(gòu)一致,本次攔截器,是統(tǒng)一做認(rèn)證,把認(rèn)證的地方統(tǒng)一放在同一個(gè)位置,而不是分散到每一個(gè)接口
若需要具體的proto源碼,可以查看我的上一期文章,如下為代碼結(jié)構(gòu)圖示

開(kāi)始書寫案例
- 在原有代碼基礎(chǔ)上加入interceptor的功能,目前案例中注冊(cè)一個(gè)攔截器
- gRPC + openssl + token + interceptor
server.go
- 主要加入
UnaryServerInterceptor來(lái)對(duì)攔截器的應(yīng)用
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認(rèn)證包
)
const (
// Address gRPC服務(wù)地址
Address = "127.0.0.1:9999"
)
// 定義helloService并實(shí)現(xiàn)約定的接口
type HiService struct{}
// HiService Hello服務(wù)
var HiSer = HiService{}
// SayHello 實(shí)現(xiàn)Hello服務(wù)接口
func (h HiService) SayHi(ctx context.Context, in *pb.HiRequest) (*pb.HiResponse, error) {
// 解析metada中的信息并驗(yàn)證
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, grpc.Errorf(codes.Unauthenticated, "no token ")
}
var (
appId string
appKey string
)
// md 是一個(gè) 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
}
// 認(rèn)證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 是一個(gè) 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) {
// 進(jìn)行認(rèn)證
log.Println("interceptor...")
err := myAuth(ctx)
if err != nil {
return nil, err
}
// 繼續(xù)處理請(qǐng)求
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認(rèn)證
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))
// 注冊(cè)一個(gè)攔截器
opts = append(opts, grpc.UnaryInterceptor(interceptor))
// 實(shí)例化grpc Server, 并開(kāi)啟TLS認(rèn)證,其中還有攔截器
s := grpc.NewServer(opts...)
// 注冊(cè)HelloService
pb.RegisterHiServer(s, HiSer)
log.Println("Listen on " + Address + " with TLS and interceptor")
s.Serve(listen)
}
client.go
- 主要加入
UnaryClientInterceptor來(lái)對(duì)攔截器的應(yīng)用
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認(rèn)證包
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服務(wù)地址
Address = "127.0.0.1:9999"
)
var IsTls = true
// myCredential 自定義認(rèn)證
type myCredential struct{}
// GetRequestMetadata 實(shí)現(xiàn)自定義認(rèn)證接口
func (c myCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "myappid",
"appkey": "mykey",
}, nil
}
// RequireTransportSecurity 自定義認(rèn)證是否開(kāi)啟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改成你寫的服務(wù)器地址
var err error
var opts []grpc.DialOption
if IsTls {
//打開(kāi)tls 走tls認(rèn)證
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())
}
// 自定義認(rèn)證,new(myCredential 的時(shí)候,由于我們實(shí)現(xiàn)了上述2個(gè)接口,因此new的時(shí)候,程序會(huì)執(zhí)行我們實(shí)現(xiàn)的接口
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)
// 調(diào)用方法
req := &pb.HiRequest{Name: "gRPC"}
res, err := c.SayHi(context.Background(), req)
if err != nil {
log.Panicln(err)
}
log.Println(res.Message)
// 故意再調(diào)用一次
res, err = c.SayHi(context.Background(), req)
if err != nil {
log.Panicln(err)
}
log.Println(res.Message)
}
實(shí)際效果展示


注意,服務(wù)器只能配置一個(gè) UnaryInterceptor和StreamClientInterceptor,否則會(huì)報(bào)錯(cuò),客戶端也是,雖然不會(huì)報(bào)錯(cuò),但是只有最后一個(gè)才起作用。 如果你想配置多個(gè),可以使用攔截器鏈,如go-grpc-middleware,或者自己實(shí)現(xiàn)。
- 服務(wù)端的攔截器
-
UnaryServerInterceptor-- 單向調(diào)用的攔截器 -
StreamServerInterceptor-- stream調(diào)用的攔截器
-
- 客戶端的攔截器
UnaryClientInterceptorStreamClientInterceptor
上述攔截器無(wú)論是單向調(diào)用的攔截器 還是 stream調(diào)用的攔截器 用法都大同小異
// 服務(wù)端
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)
最后分享社區(qū)內(nèi)用到的攔截器(還應(yīng)該有更多...)
最后與大家分享幾個(gè)社區(qū)內(nèi)用到的攔截器
用于身份驗(yàn)證攔截器
interceptor鏈?zhǔn)焦δ艿膸?kù),可以將單向的或者流式的攔截器組合
- grpc-multi-interceptor: https://github.com/kazegusuri/grpc-multi-interceptor
- go-grpc-middleware: https://github.com/grpc-ecosystem/go-grpc-middleware
為上下文增加Tag map對(duì)象
日志框架
- grpc_zap: https://github.com/grpc-ecosystem/go-grpc-middleware/blob/master/logging/zap
- logrus:https://github.com/grpc-ecosystem/go-grpc-middleware/tree/master/logging/logrus
可以為客戶端增加重試的功能
好了,本次就到這里,下一次分享 gRPC的請(qǐng)求追蹤,
技術(shù)是開(kāi)放的,我們的心態(tài),更應(yīng)是開(kāi)放的。擁抱變化,向陽(yáng)而生,努力向前行。
我是小魔童哪吒,歡迎點(diǎn)贊關(guān)注收藏,下次見(jiàn)~