Desenvolvimento de API em Go usando Goa
Publicados: 2022-03-11O desenvolvimento de API é um tema quente hoje em dia. Há um grande número de maneiras de desenvolver e entregar uma API, e grandes empresas desenvolveram soluções massivas para ajudá-lo a inicializar um aplicativo rapidamente.
No entanto, a maioria dessas opções não possui um recurso importante: gerenciamento do ciclo de vida do desenvolvimento. Assim, os desenvolvedores passam alguns ciclos criando APIs úteis e robustas, mas acabam lutando com a evolução orgânica esperada de seu código e as implicações que uma pequena mudança na API tem no código-fonte.
Em 2016, Raphael Simon criou o Goa, um framework para desenvolvimento de API em Golang com um ciclo de vida que coloca o design de API em primeiro lugar. Em Goa, sua definição de API não é apenas descrita como código, mas também é a fonte da qual o código do servidor, o código do cliente e a documentação são derivados. Isso significa que seu código é descrito em sua definição de API usando um Golang Domain Specific Language (DSL), gerado usando o goa cli e implementado separadamente do código-fonte do aplicativo.
Essa é a razão pela qual Goa brilha. É uma solução com um contrato de ciclo de vida de desenvolvimento bem definido que conta com as melhores práticas na geração de código (como dividir diferentes domínios e interesses em camadas, para que os aspectos de transporte não interfiram nos aspectos de negócios da aplicação), seguindo um padrão de arquitetura limpa onde composable módulos são gerados para as camadas de transporte, endpoint e lógica de negócios em seu aplicativo.
Alguns recursos do Goa, conforme definido pelo site oficial, incluem:
- Componibilidade . O pacote, os algoritmos de geração de código e o código gerado são todos modulares.
- Transporte-agnóstico . O desacoplamento da camada de transporte da implementação do serviço real significa que o mesmo serviço pode expor terminais acessíveis por meio de vários transportes, como HTTP e/ou gRPC.
- Separação de preocupações . A implementação do serviço real é isolada do código de transporte.
- Uso de tipos de biblioteca padrão Go . Isso facilita a interface com o código externo.
Neste artigo, criarei um aplicativo e orientarei você pelos estágios do ciclo de vida de desenvolvimento da API. O aplicativo gerencia detalhes sobre os clientes, como nome, endereço, telefone, redes sociais, etc. Ao final, tentaremos estendê-lo e adicionar novos recursos para exercitar seu ciclo de vida de desenvolvimento.
Então vamos começar!
Preparando sua área de desenvolvimento
Nosso primeiro passo é iniciar o repositório e habilitar o suporte aos módulos Go:
mkdir -p clients/design cd clients go mod init clients
No final, sua estrutura de repo deve ser como abaixo:
$ tree . ├── design └── go.mod
Projetando sua API
A fonte da verdade para sua API é sua definição de design. Como a documentação afirma, “Goa permite que você pense em suas APIs independentemente de qualquer preocupação de implementação e, em seguida, revise esse design com todas as partes interessadas antes de escrever a implementação”. Isso significa que cada elemento da API é definido aqui primeiro, antes que o código do aplicativo real seja gerado. Mas chega de falar!
Abra o arquivo clients/design/design.go
e adicione o conteúdo abaixo:
/* This is the design file. It contains the API specification, methods, inputs, and outputs using Goa DSL code. The objective is to use this as a single source of truth for the entire API source code. */ package design import ( . "goa.design/goa/v3/dsl" ) // Main API declaration var _ = API("clients", func() { Title("An api for clients") Description("This api manages clients with CRUD operations") Server("clients", func() { Host("localhost", func() { URI("http://localhost:8080/api/v1") }) }) }) // Client Service declaration with two methods and Swagger API specification file var _ = Service("client", func() { Description("The Client service allows access to client members") Method("add", func() { Payload(func() { Field(1, "ClientID", String, "Client ID") Field(2, "ClientName", String, "Client ID") Required("ClientID", "ClientName") }) Result(Empty) Error("not_found", NotFound, "Client not found") HTTP(func() { POST("/api/v1/client/{ClientID}") Response(StatusCreated) }) }) Method("get", func() { Payload(func() { Field(1, "ClientID", String, "Client ID") Required("ClientID") }) Result(ClientManagement) Error("not_found", NotFound, "Client not found") HTTP(func() { GET("/api/v1/client/{ClientID}") Response(StatusOK) }) }) Method("show", func() { Result(CollectionOf(ClientManagement)) HTTP(func() { GET("/api/v1/client") Response(StatusOK) }) }) Files("/openapi.json", "./gen/http/openapi.json") }) // ClientManagement is a custom ResultType used to configure views for our custom type var ClientManagement = ResultType("application/vnd.client", func() { Description("A ClientManagement type describes a Client of company.") Reference(Client) TypeName("ClientManagement") Attributes(func() { Attribute("ClientID", String, "ID is the unique id of the Client.", func() { Example("ABCDEF12356890") }) Field(2, "ClientName") }) View("default", func() { Attribute("ClientID") Attribute("ClientName") }) Required("ClientID") }) // Client is the custom type for clients in our database var Client = Type("Client", func() { Description("Client describes a customer of company.") Attribute("ClientID", String, "ID is the unique id of the Client Member.", func() { Example("ABCDEF12356890") }) Attribute("ClientName", String, "Name of the Client", func() { Example("John Doe Limited") }) Required("ClientID", "ClientName") }) // NotFound is a custom type where we add the queried field in the response var NotFound = Type("NotFound", func() { Description("NotFound is the type returned when " + "the requested data that does not exist.") Attribute("message", String, "Message of error", func() { Example("Client ABCDEF12356890 not found") }) Field(2, "id", String, "ID of missing data") Required("message", "id") })
A primeira coisa que você pode notar é que a DSL acima é um conjunto de funções Go que podem ser compostas para descrever uma API de serviço remoto. As funções são compostas usando argumentos de funções anônimas. Nas funções DSL, temos um subconjunto de funções que não deveriam aparecer em outras funções, que chamamos de DSLs de nível superior. Abaixo, você tem um conjunto parcial de funções DSL e sua estrutura:
Portanto, temos em nosso projeto inicial uma DSL de nível superior de API descrevendo a API de nosso cliente, uma DSL de nível superior de serviço descrevendo o principal serviço de API, clients
e servindo o arquivo swagger da API, e duas DSLs de nível superior de tipo para descrever o tipo de exibição de objeto usado na carga útil de transporte.
A função API
é uma DSL de nível superior opcional que lista as propriedades globais da API, como um nome, uma descrição e também um ou mais servidores potencialmente expondo diferentes conjuntos de serviços. No nosso caso, um servidor é suficiente, mas você também pode atender a diferentes serviços em diferentes camadas: desenvolvimento, teste e produção, por exemplo.
A função Service
define um grupo de métodos que potencialmente mapeia para um recurso no transporte. Um serviço também pode definir respostas de erro comuns. Os métodos de serviço são descritos usando Method
. Essa função define os tipos de carga útil (entrada) e resultado (saída) do método. Se você omitir a carga útil ou o tipo de resultado, o tipo interno Empty, que mapeia para um corpo vazio em HTTP, será usado.
Por fim, as funções Type
ou ResultType
definem tipos definidos pelo usuário, sendo a principal diferença que um tipo de resultado também define um conjunto de “visualizações”.
Em nosso exemplo, descrevemos a API e explicamos como ela deve servir, e também criamos o seguinte:
- Um serviço chamado
clients
- Três métodos:
add
(para criar um cliente),get
(para recuperar um cliente) eshow
(para listar todos os clientes) - Nossos próprios tipos personalizados, que serão úteis quando nos integrarmos a um banco de dados, e um tipo de erro personalizado
Agora que nosso aplicativo foi descrito, podemos gerar o código clichê. O comando a seguir usa o caminho de importação do pacote de design como um argumento. Ele também aceita o caminho para o diretório de saída como um sinalizador opcional:
goa gen clients/design
O comando gera os nomes dos arquivos que ele gera. Lá, o diretório gen
contém o subdiretório de nome do aplicativo que abriga o código de serviço independente de transporte. O subdiretório http
descreve o transporte HTTP (temos código de servidor e cliente com a lógica para codificar e decodificar solicitações e respostas, e o código CLI para construir solicitações HTTP a partir da linha de comando). Ele também contém os arquivos de especificação Open API 2.0 nos formatos JSON e YAML.
Você pode copiar o conteúdo de um arquivo swagger e colá-lo em qualquer editor Swagger online (como o de swagger.io) para visualizar sua documentação de especificação de API. Eles são compatíveis com os formatos YAML e JSON.
Agora estamos prontos para nossa próxima etapa no ciclo de vida de desenvolvimento.
Implementando sua API
Depois que seu código padrão foi criado, é hora de adicionar alguma lógica de negócios a ele. Neste ponto, é assim que seu código deve ficar:
Onde todos os arquivos acima são mantidos e atualizados pela Goa sempre que executamos a CLI. Assim, à medida que a arquitetura evolui, seu design seguirá a evolução, assim como seu código-fonte. Para implementar o aplicativo, executamos o comando abaixo (ele irá gerar uma implementação básica do serviço junto com arquivos de servidor edificáveis que acionam goroutines para iniciar um servidor HTTP e arquivos de cliente que podem fazer solicitações a esse servidor):
goa example clients/design
Isso gerará uma pasta cmd com fontes compiláveis de servidor e cliente. Haverá seu aplicativo, e esses são os arquivos que você deve manter depois que o Goa os gerar pela primeira vez.
A documentação do Goa deixa claro que: “Este comando gera um ponto de partida para o serviço para ajudar no desenvolvimento de bootstrap - em particular, NÃO deve ser executado novamente quando o design for alterado”.
Agora, seu código ficará assim:
Onde client.go
é um arquivo de exemplo com uma implementação fictícia dos métodos get
e show
. Vamos adicionar alguma lógica de negócios a ele!
Para simplificar, usaremos SQLite em vez de um banco de dados na memória e Gorm como nosso ORM. Crie o arquivo sqlite.go
e adicione o conteúdo abaixo - que irá adicionar lógica de banco de dados para criar registros no banco de dados e listar uma e/ou várias linhas do banco de dados:
package clients import ( "clients/gen/client" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Client *client.ClientManagement // InitDB is the function that starts a database file and table structures // if not created then returns db object for next functions func InitDB() *gorm.DB { // Opening file db, err := gorm.Open("sqlite3", "./data.db") // Display SQL queries db.LogMode(true) // Error if err != nil { panic(err) } // Creating the table if it doesn't exist var TableStruct = client.ClientManagement{} if !db.HasTable(TableStruct) { db.CreateTable(TableStruct) db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(TableStruct) } return db } // GetClient retrieves one client by its ID func GetClient(clientID string) (client.ClientManagement, error) { db := InitDB() defer db.Close() var clients client.ClientManagement db.Where("client_id = ?", clientID).First(&clients) return clients, err } // CreateClient created a client row in DB func CreateClient(client Client) error { db := InitDB() defer db.Close() err := db.Create(&client).Error return err } // ListClients retrieves the clients stored in Database func ListClients() (client.ClientManagementCollection, error) { db := InitDB() defer db.Close() var clients client.ClientManagementCollection err := db.Find(&clients).Error return clients, err }
Em seguida, editamos client.go para atualizar todos os métodos no client Service, implementando as chamadas do banco de dados e construindo as respostas da API:
// Add implements add. func (s *clientsrvc) Add(ctx context.Context, p *client.AddPayload) (err error) { s.logger.Print("client.add started") newClient := client.ClientManagement{ ClientID: p.ClientID, ClientName: p.ClientName, } err = CreateClient(&newClient) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.add completed") return } // Get implements get. func (s *clientsrvc) Get(ctx context.Context, p *client.GetPayload) (res *client.ClientManagement, err error) { s.logger.Print("client.get started") result, err := GetClient(p.ClientID) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.get completed") return &result, err } // Show implements show. func (s *clientsrvc) Show(ctx context.Context) (res client.ClientManagementCollection, err error) { s.logger.Print("client.show started") res, err = ListClients() if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.show completed") return }
O primeiro corte do nosso aplicativo está pronto para ser compilado. Execute o seguinte comando para criar binários de servidor e cliente:
go build ./cmd/clients go build ./cmd/clients-cli
Para executar o servidor, basta executar ./clients
. Deixe-o funcionando por enquanto. Você deve vê-lo funcionando com sucesso, como o seguinte:
$ ./clients [clients] 00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Show" mounted on GET /api/v1/client [clients] 00:00:01 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [clients] 00:00:01 HTTP server listening on "localhost:8080"
Estamos prontos para realizar alguns testes em nossa aplicação. Vamos experimentar todos os métodos usando o cli:
$ ./clients-cli client add --body '{"ClientName": "Cool Company"}' \ --client-id "1" $ ./clients-cli client get --client-id "1" { "ClientID": "1", "ClientName": "Cool Company" } $ ./clients-cli client show [ { "ClientID": "1", "ClientName": "Cool Company" } ]
Se você receber algum erro, verifique os logs do servidor para garantir que a lógica do SQLite ORM seja boa e que você não esteja enfrentando nenhum erro de banco de dados, como banco de dados não inicializado ou consultas que não retornam nenhuma linha.
Estendendo sua API
O framework suporta o desenvolvimento de plugins para estender sua API e adicionar mais recursos facilmente. Goa tem um repositório de plugins criados pela comunidade.
Como expliquei anteriormente, como parte do ciclo de vida de desenvolvimento, podemos contar com o conjunto de ferramentas para estender nosso aplicativo voltando à definição de design, atualizando-o e atualizando nosso código gerado. Vamos mostrar como os plugins podem ajudar adicionando CORS e autenticação à API.
Atualize o arquivo clients/design/design.go
para o conteúdo abaixo:
/* This is the design file. It contains the API specification, methods, inputs, and outputs using Goa DSL code. The objective is to use this as a single source of truth for the entire API source code. */ package design import ( . "goa.design/goa/v3/dsl" cors "goa.design/plugins/v3/cors/dsl" ) // Main API declaration var _ = API("clients", func() { Title("An api for clients") Description("This api manages clients with CRUD operations") cors.Origin("/.*localhost.*/", func() { cors.Headers("X-Authorization", "X-Time", "X-Api-Version", "Content-Type", "Origin", "Authorization") cors.Methods("GET", "POST", "OPTIONS") cors.Expose("Content-Type", "Origin") cors.MaxAge(100) cors.Credentials() }) Server("clients", func() { Host("localhost", func() { URI("http://localhost:8080/api/v1") }) }) }) // Client Service declaration with two methods and Swagger API specification file var _ = Service("client", func() { Description("The Client service allows access to client members") Error("unauthorized", String, "Credentials are invalid") HTTP(func() { Response("unauthorized", StatusUnauthorized) }) Method("add", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Field(2, "ClientID", String, "Client ID") Field(3, "ClientName", String, "Client ID") Field(4, "ContactName", String, "Contact Name") Field(5, "ContactEmail", String, "Contact Email") Field(6, "ContactMobile", Int, "Contact Mobile Number") Required("token", "ClientID", "ClientName", "ContactName", "ContactEmail", "ContactMobile") }) Security(JWTAuth, func() { Scope("api:write") }) Result(Empty) Error("invalid-scopes", String, "Token scopes are invalid") Error("not_found", NotFound, "Client not found") HTTP(func() { POST("/api/v1/client/{ClientID}") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusCreated) }) }) Method("get", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Field(2, "ClientID", String, "Client ID") Required("token", "ClientID") }) Security(JWTAuth, func() { Scope("api:read") }) Result(ClientManagement) Error("invalid-scopes", String, "Token scopes are invalid") Error("not_found", NotFound, "Client not found") HTTP(func() { GET("/api/v1/client/{ClientID}") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusOK) }) }) Method("show", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Required("token") }) Security(JWTAuth, func() { Scope("api:read") }) Result(CollectionOf(ClientManagement)) Error("invalid-scopes", String, "Token scopes are invalid") HTTP(func() { GET("/api/v1/client") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusOK) }) }) Files("/openapi.json", "./gen/http/openapi.json") }) // ClientManagement is a custom ResultType used to // configure views for our custom type var ClientManagement = ResultType("application/vnd.client", func() { Description("A ClientManagement type describes a Client of company.") Reference(Client) TypeName("ClientManagement") Attributes(func() { Attribute("ClientID", String, "ID is the unique id of the Client.", func() { Example("ABCDEF12356890") }) Field(2, "ClientName") Attribute("ContactName", String, "Name of the Contact.", func() { Example("John Doe") }) Field(4, "ContactEmail") Field(5, "ContactMobile") }) View("default", func() { Attribute("ClientID") Attribute("ClientName") Attribute("ContactName") Attribute("ContactEmail") Attribute("ContactMobile") }) Required("ClientID") }) // Client is the custom type for clients in our database var Client = Type("Client", func() { Description("Client describes a customer of company.") Attribute("ClientID", String, "ID is the unique id of the Client Member.", func() { Example("ABCDEF12356890") }) Attribute("ClientName", String, "Name of the Client", func() { Example("John Doe Limited") }) Attribute("ContactName", String, "Name of the Client Contact.", func() { Example("John Doe") }) Attribute("ContactEmail", String, "Email of the Client Contact", func() { Example("[email protected]") }) Attribute("ContactMobile", Int, "Mobile number of the Client Contact", func() { Example(12365474235) }) Required("ClientID", "ClientName", "ContactName", "ContactEmail", "ContactMobile") }) // NotFound is a custom type where we add the queried field in the response var NotFound = Type("NotFound", func() { Description("NotFound is the type returned " + "when the requested data that does not exist.") Attribute("message", String, "Message of error", func() { Example("Client ABCDEF12356890 not found") }) Field(2, "id", String, "ID of missing data") Required("message", "id") }) // Creds is a custom type for replying Tokens var Creds = Type("Creds", func() { Field(1, "jwt", String, "JWT token", func() { Example("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9" + "lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHD" + "cEfxjoYZgeFONFh7HgQ") }) Required("jwt") }) // JWTAuth is the JWTSecurity DSL function for adding JWT support in the API var JWTAuth = JWTSecurity("jwt", func() { Description(`Secures endpoint by requiring a valid JWT token retrieved via the signin endpoint. Supports scopes "api:read" and "api:write".`) Scope("api:read", "Read-only access") Scope("api:write", "Read and write access") }) // BasicAuth is the BasicAuth DSL function for // adding basic auth support in the API var BasicAuth = BasicAuthSecurity("basic", func() { Description("Basic authentication used to " + "authenticate security principal during signin") Scope("api:read", "Read-only access") }) // Signin Service is the service used to authenticate users and assign JWT tokens for their sessions var _ = Service("signin", func() { Description("The Signin service authenticates users and validate tokens") Error("unauthorized", String, "Credentials are invalid") HTTP(func() { Response("unauthorized", StatusUnauthorized) }) Method("authenticate", func() { Description("Creates a valid JWT") Security(BasicAuth) Payload(func() { Description("Credentials used to authenticate to retrieve JWT token") UsernameField(1, "username", String, "Username used to perform signin", func() { Example("user") }) PasswordField(2, "password", String, "Password used to perform signin", func() { Example("password") }) Required("username", "password") }) Result(Creds) HTTP(func() { POST("/signin/authenticate") Response(StatusOK) }) }) })
Você pode notar duas grandes diferenças no novo design. Definimos um escopo de segurança no serviço do client
para que possamos validar se um usuário está autorizado a invocar o serviço, e definimos um segundo serviço chamado signin
, que usaremos para autenticar usuários e gerar JSON Web Tokens (JWT), que o serviço ao client
usará para autorizar chamadas. Também adicionamos mais campos ao nosso tipo de cliente personalizado. Este é um caso comum ao desenvolver uma API – a necessidade de remodelar ou reestruturar dados.

No design, essas mudanças podem parecer simples, mas refletindo sobre elas, há muitos recursos mínimos necessários para alcançar o que é descrito no design. Veja, por exemplo, os esquemas arquitetônicos para autenticação e autorização usando nossos métodos de API:
Esses são todos os novos recursos que nosso código ainda não possui. Novamente, é aqui que Goa agrega mais valor aos seus esforços de desenvolvimento. Vamos implementar esses recursos no lado do transporte regenerando novamente o código-fonte com o comando abaixo:
goa gen clients/design
Neste ponto, se você estiver usando o Git, notará a presença de novos arquivos, com outros mostrando como atualizados. Isso ocorre porque o Goa atualizou perfeitamente o código clichê de acordo, sem nossa intervenção.
Agora, precisamos implementar o código do lado do serviço. Em um aplicativo do mundo real, você atualizaria manualmente o aplicativo após atualizar sua fonte para refletir todas as alterações de design. Esta é a maneira que Goa recomenda que prossigamos, mas por brevidade, estarei excluindo e regenerando o aplicativo de exemplo para chegarmos lá mais rápido. Execute os comandos abaixo para excluir o aplicativo de exemplo e regenerá-lo:
rm -rf cmd client.go goa example clients/design
Com isso, seu código deve ficar assim:
Podemos ver um novo arquivo em nosso aplicativo de exemplo: signin.go
, que contém a lógica do serviço de login. No entanto, podemos ver que client.go
também foi atualizado com uma função JWTAuth para validação de tokens. Isso corresponde ao que escrevemos no design, portanto, cada chamada para qualquer método no cliente será interceptada para validação de token e encaminhada apenas se autorizada por um token válido e um escopo correto.
Portanto, atualizaremos os métodos em nosso serviço de login dentro do signin.go
para adicionar a lógica para gerar os tokens que a API criará para usuários autenticados. Copie e cole o seguinte contexto em signin.go
:
package clients import ( signin "clients/gen/signin" "context" "log" "time" jwt "github.com/dgrijalva/jwt-go" "goa.design/goa/v3/security" ) // signin service example implementation. // The example methods log the requests and return zero values. type signinsrvc struct { logger *log.Logger } // NewSignin returns the signin service implementation. func NewSignin(logger *log.Logger) signin.Service { return &signinsrvc{logger} } // BasicAuth implements the authorization logic for service "signin" for the // "basic" security scheme. func (s *signinsrvc) BasicAuth(ctx context.Context, user, pass string, scheme *security.BasicScheme) (context.Context, error) { if user != "gopher" && pass != "academy" { return ctx, signin. Unauthorized("invalid username and password combination") } return ctx, nil } // Creates a valid JWT func (s *signinsrvc) Authenticate(ctx context.Context, p *signin.AuthenticatePayload) (res *signin.Creds, err error) { // create JWT token token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(), "iat": time.Now().Unix(), "exp": time.Now().Add(time.Duration(9) * time.Minute).Unix(), "scopes": []string{"api:read", "api:write"}, }) s.logger.Printf("user '%s' logged in", p.Username) // note that if "SignedString" returns an error then it is returned as // an internal error to the client t, err := token.SignedString(Key) if err != nil { return nil, err } res = &signin.Creds{ JWT: t, } return }
Por fim, como adicionamos mais campos ao nosso tipo personalizado, precisamos atualizar o método Add no client Service em client.go
para refletir essas alterações. Copie e cole o seguinte para atualizar seu client.go
:
package clients import ( client "clients/gen/client" "context" "log" jwt "github.com/dgrijalva/jwt-go" "goa.design/goa/v3/security" ) var ( // Key is the key used in JWT authentication Key = []byte("secret") ) // client service example implementation. // The example methods log the requests and return zero values. type clientsrvc struct { logger *log.Logger } // NewClient returns the client service implementation. func NewClient(logger *log.Logger) client.Service { return &clientsrvc{logger} } // JWTAuth implements the authorization logic for service "client" for the // "jwt" security scheme. func (s *clientsrvc) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) { claims := make(jwt.MapClaims) // authorize request // 1. parse JWT token, token key is hardcoded to "secret" in this example _, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { return Key, nil }) if err != nil { s.logger.Print("Unable to obtain claim from token, it's invalid") return ctx, client.Unauthorized("invalid token") } s.logger.Print("claims retrieved, validating against scope") s.logger.Print(claims) // 2. validate provided "scopes" claim if claims["scopes"] == nil { s.logger.Print("Unable to get scope since the scope is empty") return ctx, client.InvalidScopes("invalid scopes in token") } scopes, ok := claims["scopes"].([]interface{}) if !ok { s.logger.Print("An error occurred when retrieving the scopes") s.logger.Print(ok) return ctx, client.InvalidScopes("invalid scopes in token") } scopesInToken := make([]string, len(scopes)) for _, scp := range scopes { scopesInToken = append(scopesInToken, scp.(string)) } if err := scheme.Validate(scopesInToken); err != nil { s.logger.Print("Unable to parse token, check error below") return ctx, client.InvalidScopes(err.Error()) } return ctx, nil } // Add implements add. func (s *clientsrvc) Add(ctx context.Context, p *client.AddPayload) (err error) { s.logger.Print("client.add started") newClient := client.ClientManagement{ ClientID: p.ClientID, ClientName: p.ClientName, ContactName: p.ContactName, ContactEmail: p.ContactEmail, ContactMobile: p.ContactMobile, } err = CreateClient(&newClient) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.add completed") return } // Get implements get. func (s *clientsrvc) Get(ctx context.Context, p *client.GetPayload) (res *client.ClientManagement, err error) { s.logger.Print("client.get started") result, err := GetClient(p.ClientID) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.get completed") return &result, err } // Show implements show. func (s *clientsrvc) Show(ctx context.Context, p *client.ShowPayload) (res client.ClientManagementCollection, err error) { s.logger.Print("client.show started") res, err = ListClients() if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.show completed") return }
E é isso! Vamos recompilar o aplicativo e testá-lo novamente. Execute os comandos abaixo para remover os binários antigos e compilar os novos:
rm -f clients clients-cli go build ./cmd/clients go build ./cmd/clients-cli
Execute ./clients
novamente e deixe-o em execução. Você deve vê-lo rodando com sucesso, mas desta vez, com os novos métodos implementados:
$ ./clients [clients] 00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Show" mounted on GET /api/v1/client [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /openapi.json [clients] 00:00:01 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [clients] 00:00:01 HTTP "Authenticate" mounted on POST /signin/authenticate [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /signin/authenticate [clients] 00:00:01 HTTP server listening on "localhost:8080"
Para testar, vamos executar todos os métodos da API usando o cli — observe que estamos usando as credenciais codificadas:
$ ./clients-cli signin authenticate \ --username "gopher" --password "academy" { "JWT": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4 \ MSwibmJmIjoxNDQ0NDc4NDAwLCJzY29wZXMiOlsiY \ XBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" } $ ./clients-cli client add --body \ '{"ClientName": "Cool Company", \ "ContactName": "Jane Masters", \ "ContactEmail": "[email protected]", \ "ContactMobile": 13426547654 }' \ --client-id "1" --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" $ ./clients-cli client get --client-id "1" \ --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" { "ClientID": "1", "ClientName": "Cool Company", "ContactName": "Jane Masters", "ContactEmail": "[email protected]", "ContactMobile": 13426547654 } $ ./clients-cli client show \ --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" [ { "ClientID": "1", "ClientName": "Cool Company", "ContactName": "Jane Masters", "ContactEmail": "[email protected]", "ContactMobile": 13426547654 } ]
E lá vamos nós! Temos um aplicativo minimalista com autenticação adequada, autorização de escopo e espaço para crescimento evolutivo. Depois disso, você pode desenvolver sua própria estratégia de autenticação usando serviços em nuvem ou qualquer outro provedor de identidade de sua escolha. Você também pode criar plugins para seu banco de dados ou sistema de mensagens preferido, ou até mesmo integrar com outras APIs facilmente.
Confira o projeto GitHub da Goa para mais plugins, exemplos (mostrando recursos específicos do framework) e outros recursos úteis.
Por hoje é isso. Espero que você tenha gostado de jogar com Goa e ler este artigo. Se você tiver algum feedback sobre o conteúdo, sinta-se à vontade para entrar em contato no GitHub, Twitter ou LinkedIn.
Além disso, nós saímos no canal #goa no Gophers Slack, então venha e diga oi!
Para obter mais informações sobre Golang, consulte Go Programming Language: An Introductory Golang Tutorial and Well-structured Logic: A Golang OOP Tutorial.