Go+gRPC-Gateway(V2) 微服務實戰,小程式登入鑑權服務(五):鑑權 gRPC-Interceptor 攔截器實戰

為少發表於2021-04-19

攔截器(gRPC-Interceptor)類似於 Gin 中介軟體(Middleware),讓你在真正呼叫 RPC 服務前,進行身份認證、引數校驗、限流等通用操作。

系列

  1. 雲原生 API 閘道器,gRPC-Gateway V2 初探
  2. Go + gRPC-Gateway(V2) 構建微服務實戰系列,小程式登入鑑權服務:第一篇
  3. Go + gRPC-Gateway(V2) 構建微服務實戰系列,小程式登入鑑權服務:第二篇
  4. Go + gRPC-Gateway(V2) 構建微服務實戰系列,小程式登入鑑權服務(三):RSA(RS512) 簽名 JWT
  5. Go+gRPC-Gateway(V2) 微服務實戰,小程式登入鑑權服務(四):自動生成 API TS 型別

grpc.UnaryInterceptor

VSCode -> Go to Definition 開始,我們看到如下原始碼:

// UnaryInterceptor returns a ServerOption that sets the UnaryServerInterceptor for the
// server. Only one unary interceptor can be installed. The construction of multiple
// interceptors (e.g., chaining) can be implemented at the caller.
func UnaryInterceptor(i UnaryServerInterceptor) ServerOption {
	return newFuncServerOption(func(o *serverOptions) {
		if o.unaryInt != nil {
			panic("The unary server interceptor was already set and may not be reset.")
		}
		o.unaryInt = i
	})
}

註釋很清晰:UnaryInterceptor 返回一個為 gRPC server 設定 UnaryServerInterceptorServerOption。只能安裝一個一元攔截器。多個攔截器的構造(例如,chaining)可以在呼叫方實現。

這裡我們需要實現具有如下定義的方法:

// UnaryServerInterceptor provides a hook to intercept the execution of a unary RPC on the server. info
// contains all the information of this RPC the interceptor can operate on. And handler is the wrapper
// of the service method implementation. It is the responsibility of the interceptor to invoke handler
// to complete the RPC.
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

註釋很清晰:UnaryServerInterceptor 提供了一個鉤子來攔截伺服器上一元 RPC 的執行。info 包含攔截器可以操作的這個 RPC 的所有資訊。handlerservice 方法實現的包裝器。攔截器的職責是呼叫 handler 來完成 RPC 方法的執行。在真正呼叫 RPC 服務前,進行各微服務的通用操作(如:authorization)。

Auth Interceptor 編寫

一句話描述業務:

  • 從請求頭(header) 中拿到 authorization 欄位傳過來的 token,然後通過 pubclic.key 驗證是否合法。合法就把 AccountID(claims.subject) 附加到當前請求上下文中(context)。

核心攔截器程式碼如下:

type interceptor struct {
	verifier tokenVerifier
}
func (i *interceptor) HandleReq(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    // 拿到 token
	tkn, err := tokenFromContext(ctx)
	if err != nil {
		return nil, status.Error(codes.Unauthenticated, "")
	}
    
    // 驗證 token
	aid, err := i.verifier.Verify(tkn)
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, "token not valid: %v", err)
	}
	// 呼叫真正的 RPC 方法
	return handler(ContextWithAccountID(ctx, AccountID(aid)), req)
}

具體程式碼位於 /microsvcs/shared/auth/auth.go

Todo 微服務

一個 Todo-List 測試服務。

這裡,我們加入一個新的微服務 Todo,我們要做的是:訪問 Todo RPC Service 之前需要經過我們的鑑權 Interceptor 判斷是否合法。

定義 proto

todo.proto

syntax = "proto3";
package todo.v1;
option go_package="server/todo/api/gen/v1;todopb";
message CreateTodoRequest {
    string title = 1;
}
message CreateTodoResponse {
}
service TodoService {
    rpc CreateTodo (CreateTodoRequest) returns (CreateTodoResponse);
}

簡單起見(測試用),這裡就一個欄位 title

定義 google.api.Service

todo.yaml

type: google.api.Service
config_version: 3

http:
  rules:
  - selector: todo.v1.TodoService.CreateTodo
    post: /v1/todo
    body: "*"

生成相關程式碼

microsvcs 目錄下執行:

sh gen.sh

會生成如下檔案:

  • microsvcs/todo/api/gen/v1/todo_grpc.pb.go
  • microsvcs/todo/api/gen/v1/todo.pb.go
  • microsvcs/todo/api/gen/v1/todo.pb.gw.go

client 目錄下執行:

sh gen_ts.sh

會生成如下檔案:

  • client/miniprogram/service/proto_gen/todo/todo_pb.js
  • client/miniprogram/service/proto_gen/todo/todo_pb.d.ts

實現 CreateTodo Service

具體見:microsvcs/todo/todo/todo.go

type Service struct {
	Logger *zap.Logger
	todopb.UnimplementedTodoServiceServer
}
func (s *Service) CreateTodo(c context.Context, req *todopb.CreateTodoRequest) (*todopb.CreateTodoResponse, error) {
    // 從 token 中解析出 accountId,確定身份後執行後續操作
	aid, err := auth.AcountIDFromContext(c)
	if err != nil {
		return nil, err
	}
	s.Logger.Info("create trip", zap.String("title", req.Title), zap.String("account_id", aid.String()))
	return nil, status.Error(codes.Unimplemented, "")
}

重構下 gRPC-Server 的啟動

我們現在有多個服務了,Server 啟動部分有很多重複的,重構一下:

具體程式碼位於:microsvcs/shared/server/grpc.go

func RunGRPCServer(c *GRPCConfig) error {
	nameField := zap.String("name", c.Name)
	lis, err := net.Listen("tcp", c.Addr)
	if err != nil {
		c.Logger.Fatal("cannot listen", nameField, zap.Error(err))
	}
	var opts []grpc.ServerOption
	// 鑑權微服務是無需 auth 攔截器,這裡做一下判斷
	if c.AuthPublicKeyFile != "" {
		in, err := auth.Interceptor(c.AuthPublicKeyFile)
		if err != nil {
			c.Logger.Fatal("cannot create auth interceptor", nameField, zap.Error(err))
		}
		opts = append(opts, grpc.UnaryInterceptor(in))
	}
	s := grpc.NewServer(opts...)
	c.RegisterFunc(s)
	c.Logger.Info("server started", nameField, zap.String("addr", c.Addr))
	return s.Serve(lis)
}

接下,其它微服務的gRPC-Server啟動程式碼就好看很多了:

具體程式碼位於:todo/main.go

logger.Sugar().Fatal(
    server.RunGRPCServer(&server.GRPCConfig{
    	Name:              "todo",
    	Addr:              ":8082",
    	AuthPublicKeyFile: "shared/auth/public.key",
    	Logger:            logger,
    	RegisterFunc: func(s *grpc.Server) {
    		todopb.RegisterTodoServiceServer(s, &todo.Service{
    			Logger: logger,
    		})
    	},
    }),
)

具體程式碼位於:auth/main.go

logger.Sugar().Fatal(
	server.RunGRPCServer(&server.GRPCConfig{
		Name:   "auth",
		Addr:   ":8081",
		Logger: logger,
		RegisterFunc: func(s *grpc.Server) {
			authpb.RegisterAuthServiceServer(s, &auth.Service{
				OpenIDResolver: &wechat.Service{
					AppID:     "your-appid",
					AppSecret: "your-appsecret",
				},
				Mongo:          dao.NewMongo(mongoClient.Database("grpc-gateway-auth")),
				Logger:         logger,
				TokenExpire:    2 * time.Hour,
				TokenGenerator: token.NewJWTTokenGen("server/auth", privKey),
			})
		},
	}),
)

聯調

重構下 gateway server

我們要反向代理到多個 gRPC server 端點了,整理下程式碼,弄成配置的形式:

具體程式碼位於:microsvcs/gateway/main.go

serverConfig := []struct {
	name         string
	addr         string
	registerFunc func(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error)
}{
	{
		name:         "auth",
		addr:         "localhost:8081",
		registerFunc: authpb.RegisterAuthServiceHandlerFromEndpoint,
	},
	{
		name:         "todo",
		addr:         "localhost:8082",
		registerFunc: todopb.RegisterTodoServiceHandlerFromEndpoint,
	},
}

for _, s := range serverConfig {
	err := s.registerFunc(
		c, mux, s.addr,
		[]grpc.DialOption{grpc.WithInsecure()},
	)
	if err != nil {
		logger.Sugar().Fatalf("cannot register service %s : %v", s.name, err)
	}
}
addr := ":8080"
logger.Sugar().Infof("grpc gateway started at %s", addr)
logger.Sugar().Fatal(http.ListenAndServe(addr, mux))

測試

Refs

我是為少
微信:uuhells123
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)

相關文章