编写微服务博客-API网关的第2部分

我们系列文章“在微服务上写博客”第一部分中,我们描述了解决问题的通用方法。

现在轮到API网关或GW API了。

在我们的ptimofeev GW API中,我们实现了以下功能:

  • 将REST请求转换为gRPC请求,反之亦然。
  • 请求记录。
  • 请求认证
  • 为每个请求分配一个跟踪ID,以便在整个请求执行链中的微服务之间进一步传输。

所以走吧...

为了实现REST / gRPC转换功能,我们将使用gosh库grpc-gateway

此外,在我们要在REST上发布的每个微服务的原型中,您需要在服务接口的description部分中添加选项描述。 它实际上指定了执行REST访问的路径和方法。

//  Category service CategoryService { //  rpc Create (CreateCategoryRequest) returns (CreateCategoryResponse) { option (google.api.http) = { post: "/api/v1/category" }; } } 

根据此信息,代码生成脚本(./bin/protogen.sh)将创建gRPC服务器代码(在微服务目录中),gRPC客户端(在api-gw目录中)并生成最新的API文档(格式为{{service name}})。 swagger.json)

接下来,我们需要编写HTTP代理代码,该代码一方面将是HTTP服务器(用于处理REST请求),另一方面将是我们的微服务的gRPC客户端(gRPC服务器)。

我们会将这段代码放在文件./services/api-gw/main.go中。

首先,在导入部分,我们将客户端库连接到我们的微服务
(protogen.sh为我们生成了它们):

 import ( … userService "./services/user/protobuf" postService "./services/post/protobuf" commentService "./services/comment/protobuf" categoryService "./services/category/protobuf" 

接下来,我们指示gRPC服务“挂起”的地址和端口(我们从环境变量中获取值):

 var ( // gRPC services userServerAdress=fmt.Sprintf("%s:%s",os.Getenv("USER_HOST"),os.Getenv("USER_PORT")) postServerAdress=fmt.Sprintf("%s:%s",os.Getenv("POST_HOST"),os.Getenv("POST_PORT")) commentServerAdress=fmt.Sprintf("%s:%s",os.Getenv("COMMENT_HOST"),os.Getenv("COMMENT_PORT")) categoryServerAdress=fmt.Sprintf("%s:%s",os.Getenv("CATEGORY_HOST"),os.Getenv("CATEGORY_PORT")) ) 

最后,我们实现HTTP代理本身:

 func HTTPProxy(proxyAddr string){ grpcGwMux:=runtime.NewServeMux() //---------------------------------------------------------------- //     gRPC //---------------------------------------------------------------- //   User grpcUserConn, err:=grpc.Dial( userServerAdress, grpc.WithInsecure(), ) if err!=nil{ log.Fatalln("Failed to connect to User service", err) } defer grpcUserConn.Close() err = userService.RegisterUserServiceHandler( context.Background(), grpcGwMux, grpcUserConn, ) if err!=nil{ log.Fatalln("Failed to start HTTP server", err) } //---------------------------------------------------------------- //   Post grpcPostConn, err:=grpc.Dial( postServerAdress, grpc.WithUnaryInterceptor(AccessLogInterceptor), grpc.WithInsecure(), ) if err!=nil{ log.Fatalln("Failed to connect to Post service", err) } defer grpcPostConn.Close() err = postService.RegisterPostServiceHandler( context.Background(), grpcGwMux, grpcPostConn, ) if err!=nil{ log.Fatalln("Failed to start HTTP server", err) } //---------------------------------------------------------------- //   Comment grpcCommentConn, err:=grpc.Dial( commentServerAdress, grpc.WithInsecure(), ) if err!=nil{ log.Fatalln("Failed to connect to Comment service", err) } defer grpcCommentConn.Close() err = commentService.RegisterCommentServiceHandler( context.Background(), grpcGwMux, grpcCommentConn, ) if err!=nil{ log.Fatalln("Failed to start HTTP server", err) } //---------------------------------------------------------------- //   Category grpcCategoryConn, err:=grpc.Dial( categoryServerAdress, grpc.WithInsecure(), ) if err!=nil{ log.Fatalln("Failed to connect to Category service", err) } defer grpcCategoryConn.Close() err = categoryService.RegisterCategoryServiceHandler( context.Background(), grpcGwMux, grpcCategoryConn, ) if err!=nil{ log.Fatalln("Failed to start HTTP server", err) } //---------------------------------------------------------------- //     REST //---------------------------------------------------------------- mux:=http.NewServeMux() mux.Handle("/api/v1/",grpcGwMux) mux.HandleFunc("/",helloworld) fmt.Println("starting HTTP server at "+proxyAddr) log.Fatal(http.ListenAndServe(proxyAddr,mux)) } 

在建立与微服务的连接时,我们使用grpc.WithUnaryInterceptor(AccessLogInterceptor)选项,我们将AccessLogInterceptor函数作为参数传递给该选项。 这无非就是中间件层的实现,即 AccessLogInterceptor函数将在子微服务的每个gRPC调用中执行。

 //---------------------------------------------------------------- //   Post grpcPostConn, err:=grpc.Dial( … grpc.WithUnaryInterceptor(AccessLogInterceptor), … ) 

反过来,在AccessLogInterceptor函数中,我们已经实现了身份验证,日志记录和TraceId生成机制。

如果在传入(REST)请求的标头中指定了授权属性,则我们将在CheckGetJWTToken函数中对其进行解析和验证,该函数将返回错误,或者如果成功,则返回UserId和UserRole。

 var traceId,userId,userRole string if len(md["authorization"])>0{ tokenString:= md["authorization"][0] if tokenString!=""{ err,token:=userService.CheckGetJWTToken(tokenString) if err!=nil{ return err } userId=fmt.Sprintf("%s",token["UserID"]) userRole=fmt.Sprintf("%s",token["UserRole"]) } } 

接下来,我们形成TraceId并将其与UserId和UserRole一起包装在调用上下文中,并执行我们的微服务的gRPC调用。

 // ID  traceId=fmt.Sprintf("%d",time.Now().UTC().UnixNano()) callContext:=context.Background() mdOut:=metadata.Pairs( "trace-id",traceId, "user-id",userId, "user-role",userRole, ) callContext=metadata.NewOutgoingContext(callContext,mdOut) err:=invoker(callContext,method,req,reply,cc, opts...) 

最后,我们将服务调用事件写入日志。

 msg:=fmt.Sprintf("Call:%v, traceId: %v, userId: %v, userRole: %v, time: %v", method,traceId,userId,userRole,time.Since(start)) app.AccesLog(msg) 

另一个中间件处理器正在“垂悬”用户服务的特定方法(SignIn,SignUp)的答案。 该处理程序拦截gRPC响应,获取UserID和UserRole响应,将其转换为JWT令牌,并在REST响应中将其(JWT令牌)作为“授权”属性标头提供。 所描述的中间件代码在文件./api-gw/services/user/protobuf/functions.go中的gRPC客户端上实现。

我们连接响应处理程序。

 func init() { //     SignIn forward_UserService_SignIn_0 = forwardSignIn //     SignUp forward_UserService_SignUp_0 = forwardSignUp } 

一个示例是SignIn响应处理程序(SignUp处理程序与此类似)。

 func forwardSignIn(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, req *http.Request, resp proto.Message, opts ...func(context.Context, http.ResponseWriter, proto.Message) error) { // proto.Message  SignInResponse signInResponse:=&SignInResponse{} signInResponse.XXX_Merge(resp) token,err:=GetJWTToken(signInResponse.Slug,signInResponse.Role) if err!=nil{ http.Error(w, fmt.Sprintf("%v",err), http.StatusUnauthorized) return } w.Header().Set("authorization", token) runtime.ForwardResponseMessage(ctx, mux, marshaler, w, req, resp, opts...) } 

待续...

是的,可以在此处查看项目的演示,源代码在此处

Source: https://habr.com/ru/post/zh-CN473516/


All Articles