[TOC]
瞧一瞧 gRPC 的拦截器
上一次说到 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: https://github.com/grpc-ecosy…
interceptor 链式性能的库,能够将单向的或者流式的拦截器组合
- grpc-multi-interceptor: https://github.com/kazegusuri…
- go-grpc-middleware: https://github.com/grpc-ecosy…
为上下文减少Tag
map 对象
- grpc_ctxtags: https://github.com/grpc-ecosy…
日志框架
- grpc_zap: https://github.com/grpc-ecosy…
- logrus:https://github.com/grpc-ecosy…
能够为客户端减少重试的性能
- grpc_retry: https://github.com/grpc-ecosy…
好了,本次就到这里,下一次分享 gRPC 的申请追踪,
技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。
我是 小魔童哪吒,欢送点赞关注珍藏,下次见~