Escrevendo um blog sobre microsserviços - Parte 2 do API Gateway

Na primeira parte de nossa série de artigos "Escrevendo um blog sobre microsserviços" , descrevemos uma abordagem geral para resolver o problema.

Agora é a vez do API Gateway ou GW API.

Em nossa API GW c ptimofeev , implementamos as seguintes funções:

  • Convertendo solicitações REST em solicitações de gRPC e vice-versa.
  • Solicitar log.
  • Solicitar autenticação
  • Designação de um ID de Rastreio para cada solicitação para posterior transferência entre microsserviços ao longo de toda a cadeia de execução da solicitação.

Então vamos lá ...

Para implementar a função de conversão REST / gRPC, usaremos a biblioteca gosh grpc-gateway .

Além disso, no protofile de cada microsserviço que queremos publicar no REST, você precisa adicionar uma descrição da opção na seção de descrição das interfaces de serviço. Na verdade, ele especifica o caminho e o método pelo qual o acesso REST será executado.

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

Com base nessas informações, o script de geração de código (./bin/protogen.sh) criará o código do servidor gRPC (no diretório microservice), o cliente gRPC (no diretório api-gw) e gerará a documentação mais recente da API (no formato {{service name}}. swagger.json)

Em seguida, precisamos escrever o código HTTP Proxy, que por um lado será um servidor HTTP (para processar solicitações REST) ​​e, por outro, será um cliente gRPC para nossos microsserviços (servidores gRPC).

Colocaremos esse código no arquivo ./services/api-gw/main.go.

Primeiro, na seção de importação, conectamos bibliotecas clientes aos nossos microsserviços
(protogen.sh os gerou para nós):

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

A seguir, indicamos os endereços e portas nas quais nossos serviços gRPC “travam” (pegamos os valores das variáveis ​​de ambiente):

 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")) ) 

E, finalmente, implementamos o próprio proxy 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)) } 

Ao configurar a conexão com microsserviços, usamos a opção grpc.WithUnaryInterceptor (AccessLogInterceptor), na qual passamos a função AccessLogInterceptor como parâmetro. Isso nada mais é do que uma implementação da camada de middleware, ou seja, a função AccessLogInterceptor será executada a cada chamada gRPC do microsserviço filho.

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

Por sua vez, na função AccessLogInterceptor, já implementamos mecanismos de autenticação, log e geração de TraceId.

Se o atributo de autorização foi especificado no cabeçalho na solicitação de entrada (REST), analisamos e validamos na função CheckGetJWTToken, que retorna um erro ou, se bem-sucedido, retorna UserId e 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"]) } } 

Em seguida, formamos o TraceId e o agrupamos com UserId e UserRole no contexto da chamada e realizamos a chamada gRPC do nosso microsserviço.

 // 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...) 

E, finalmente, escrevemos um evento de chamada de serviço no log.

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

Outro processador de middleware está "travando" nas respostas de métodos específicos (SignIn, SignUp) do serviço do Usuário. Esse manipulador intercepta respostas gRPC, pega as respostas UserID e UserRole, converte-as em JWT Token e as fornece (JWT Token) na resposta REST como o cabeçalho de atributo "Authorization". O código do middleware descrito é implementado no lado do cliente gRPC no arquivo ./api-gw/services/user/protobuf/functions.go.

Conectamos o manipulador de respostas.

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

Um exemplo é o manipulador de respostas SignIn (o manipulador SignUp é semelhante).

 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...) } 

Para continuar ...

Sim, a demonstração do projeto pode ser vista aqui e o código fonte está aqui .

Source: https://habr.com/ru/post/pt473516/


All Articles