Desarrollo de API en Go usando Goa
Publicado: 2022-03-11El desarrollo de API es un tema candente hoy en día. Hay una gran cantidad de formas en que puede desarrollar y entregar una API, y las grandes empresas han desarrollado soluciones masivas para ayudarlo a iniciar una aplicación rápidamente.
Sin embargo, la mayoría de esas opciones carecen de una característica clave: la gestión del ciclo de vida del desarrollo. Por lo tanto, los desarrolladores pasan algunos ciclos creando API útiles y sólidas, pero terminan luchando con la evolución orgánica esperada de su código y las implicaciones que tiene un pequeño cambio en la API en el código fuente.
En 2016, Raphael Simon creó Goa, un marco para el desarrollo de API en Golang con un ciclo de vida que prioriza el diseño de API. En Goa, su definición de API no solo se describe como código, sino que también es la fuente de la que se derivan el código del servidor, el código del cliente y la documentación. Esto significa que su código se describe en su definición de API usando un lenguaje específico de dominio (DSL) de Golang, luego se genera usando goa cli y se implementa por separado del código fuente de su aplicación.
Esa es la razón por la que Goa brilla. Es una solución con un contrato de ciclo de vida de desarrollo bien definido que se basa en las mejores prácticas al generar código (como dividir diferentes dominios y preocupaciones en capas, para que los aspectos de transporte no interfieran con los aspectos comerciales de la aplicación), siguiendo un patrón de arquitectura limpio donde componible Los módulos se generan para las capas de transporte, punto final y lógica comercial en su aplicación.
Algunas características de Goa, tal como se definen en el sitio web oficial, incluyen:
- Componibilidad . El paquete, los algoritmos de generación de código y el código generado son todos modulares.
- Transporte-agnóstico . El desacoplamiento de la capa de transporte de la implementación real del servicio significa que el mismo servicio puede exponer puntos finales accesibles a través de múltiples transportes, como HTTP y/o gRPC.
- Separación de preocupaciones . La implementación real del servicio está aislada del código de transporte.
- Uso de los tipos de biblioteca estándar de Go . Esto facilita la interfaz con el código externo.
En este artículo, crearé una aplicación y lo guiaré a través de las etapas del ciclo de vida de desarrollo de la API. La aplicación administra detalles sobre los clientes, como el nombre, la dirección, el número de teléfono, las redes sociales, etc. Al final, intentaremos ampliarla y agregar nuevas funciones para ejercitar su ciclo de vida de desarrollo.
¡Entonces empecemos!
Preparación de su área de desarrollo
Nuestro primer paso es iniciar el repositorio y habilitar la compatibilidad con los módulos Go:
mkdir -p clients/design cd clients go mod init clients
Al final, la estructura de su repositorio debería ser como la siguiente:
$ tree . ├── design └── go.mod
Diseñando tu API
La fuente de la verdad para su API es su definición de diseño. Como dice la documentación, "Goa le permite pensar en sus API independientemente de cualquier problema de implementación y luego revisar ese diseño con todas las partes interesadas antes de escribir la implementación". Esto significa que cada elemento de la API se define aquí primero, antes de que se genere el código de la aplicación real. ¡Pero basta de hablar!
Abra el archivo clients/design/design.go
y agregue el contenido a continuación:
/* 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") })
Lo primero que puede notar es que el DSL anterior es un conjunto de funciones de Go que se pueden componer para describir una API de servicio remoto. Las funciones se componen utilizando argumentos de función anónimos. En las funciones de DSL, tenemos un subconjunto de funciones que no deberían aparecer dentro de otras funciones, a las que llamamos DSL de nivel superior. A continuación, tiene un conjunto parcial de funciones DSL y su estructura:
Por lo tanto, tenemos en nuestro diseño inicial un DSL de nivel superior de API que describe la API de nuestro cliente, un DSL de nivel superior de servicio que describe el servicio de API principal, los clients
y sirve el archivo swagger de API, y dos tipos de DSL de nivel superior para describir el tipo de vista de objeto utilizado en la carga útil de transporte.
La función API
es un DSL de nivel superior opcional que enumera las propiedades globales de la API, como un nombre, una descripción y también uno o más servidores que potencialmente exponen diferentes conjuntos de servicios. En nuestro caso, un servidor es suficiente, pero también podría brindar diferentes servicios en diferentes niveles: desarrollo, prueba y producción, por ejemplo.
La función Service
define un grupo de métodos que potencialmente se asignan a un recurso en el transporte. Un servicio también puede definir respuestas de error comunes. Los métodos de servicio se describen utilizando Method
. Esta función define los tipos de carga útil del método (entrada) y resultado (salida). Si omite la carga útil o el tipo de resultado, se utiliza el tipo integrado Vacío, que se asigna a un cuerpo vacío en HTTP.
Finalmente, las funciones Type
o ResultType
definen tipos definidos por el usuario, la principal diferencia es que un tipo de resultado también define un conjunto de "vistas".
En nuestro ejemplo, describimos la API y explicamos cómo debería funcionar, y también creamos lo siguiente:
- Un servicio llamado
clients
- Tres métodos:
add
(para crear un cliente),get
(para recuperar un cliente) yshow
(para enumerar todos los clientes) - Nuestros propios tipos personalizados, que serán útiles cuando nos integremos con una base de datos, y un tipo de error personalizado.
Ahora que se ha descrito nuestra aplicación, podemos generar el código repetitivo. El siguiente comando toma la ruta de importación del paquete de diseño como argumento. También acepta la ruta al directorio de salida como un indicador opcional:
goa gen clients/design
El comando genera los nombres de los archivos que genera. Allí, el directorio gen
contiene el subdirectorio del nombre de la aplicación que alberga el código de servicio independiente del transporte. El subdirectorio http
describe el transporte HTTP (tenemos código de servidor y cliente con la lógica para codificar y decodificar solicitudes y respuestas, y el código CLI para crear solicitudes HTTP desde la línea de comandos). También contiene los archivos de especificación de Open API 2.0 en formato JSON y YAML.
Puede copiar el contenido de un archivo swagger y pegarlo en cualquier editor de Swagger en línea (como el de swagger.io) para visualizar la documentación de especificación de su API. Admiten los formatos YAML y JSON.
Ahora estamos listos para nuestro siguiente paso en el ciclo de vida del desarrollo.
Implementando su API
Después de que se haya creado su código repetitivo, es hora de agregarle algo de lógica comercial. En este punto, así es como debería verse su código:
Donde todos los archivos anteriores son mantenidos y actualizados por Goa cada vez que ejecutamos la CLI. Por lo tanto, a medida que la arquitectura evolucione, su diseño seguirá la evolución, al igual que su código fuente. Para implementar la aplicación, ejecutamos el siguiente comando (generará una implementación básica del servicio junto con archivos de servidor compilables que activan rutinas para iniciar un servidor HTTP y archivos de clientes que pueden realizar solicitudes a ese servidor):
goa example clients/design
Esto generará una carpeta cmd con fuentes compilables tanto del servidor como del cliente. Habrá su aplicación, y esos son los archivos que debe mantener usted mismo después de que Goa los genere por primera vez.
La documentación de Goa deja en claro que: "Este comando genera un punto de partida para el servicio para ayudar al desarrollo de arranque; en particular, NO está destinado a volver a ejecutarse cuando cambia el diseño".
Ahora, su código se verá así:
Donde client.go
es un archivo de ejemplo con una implementación ficticia de los métodos get
y show
. ¡Agreguémosle algo de lógica comercial!
Para simplificar, usaremos SQLite en lugar de una base de datos en memoria y Gorm como nuestro ORM. Cree el archivo sqlite.go
y agregue el contenido a continuación, que agregará la lógica de la base de datos para crear registros en la base de datos y enumerar una o varias filas de la base de datos:
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 }
Luego, editamos client.go para actualizar todos los métodos en el servicio del cliente, implementando las llamadas a la base de datos y construyendo las respuestas de la 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 }
El primer corte de nuestra aplicación está listo para ser compilado. Ejecute el siguiente comando para crear binarios de servidor y cliente:
go build ./cmd/clients go build ./cmd/clients-cli
Para ejecutar el servidor, simplemente ejecute ./clients
. Déjalo funcionando por ahora. Debería verlo funcionando correctamente, como el siguiente:
$ ./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 listos para realizar algunas pruebas en nuestra aplicación. Probemos todos los métodos usando el 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" } ]
Si obtiene algún error, verifique los registros del servidor para asegurarse de que la lógica ORM de SQLite sea buena y que no se enfrente a ningún error de la base de datos, como una base de datos no inicializada o consultas que no devuelvan filas.
Extendiendo su API
El marco admite el desarrollo de complementos para ampliar su API y agregar más funciones fácilmente. Goa tiene un repositorio de complementos creados por la comunidad.
Como expliqué anteriormente, como parte del ciclo de vida del desarrollo, podemos confiar en el conjunto de herramientas para ampliar nuestra aplicación volviendo a la definición del diseño, actualizándola y refrescando nuestro código generado. Mostremos cómo los complementos pueden ayudar agregando CORS y autenticación a la API.
Actualice el archivo clients/design/design.go
al siguiente contenido:
/* 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) }) }) })
Puede notar dos diferencias importantes en el nuevo diseño. Definimos un alcance de seguridad en el servicio del client
para que podamos validar si un usuario está autorizado para invocar el servicio, y definimos un segundo servicio llamado inicio de signin
, que usaremos para autenticar a los usuarios y generar JSON Web Tokens (JWT), que el el servicio de atención al client
utilizará para autorizar las llamadas. También hemos agregado más campos a nuestro tipo de cliente personalizado. Este es un caso común cuando se desarrolla una API: la necesidad de remodelar o reestructurar los datos.

En el diseño, estos cambios pueden parecer simples, pero al reflexionar sobre ellos, se requieren muchas características mínimas para lograr lo que se describe en el diseño. Tomemos, por ejemplo, los esquemas arquitectónicos para la autenticación y autorización utilizando nuestros métodos API:
Esas son todas las características nuevas que nuestro código aún no tiene. Nuevamente, aquí es donde Goa agrega más valor a sus esfuerzos de desarrollo. Implementemos estas características en el lado del transporte regenerando nuevamente el código fuente con el siguiente comando:
goa gen clients/design
En este punto, si está utilizando Git, notará la presencia de nuevos archivos, y otros se mostrarán como actualizados. Esto se debe a que Goa actualizó sin problemas el código repetitivo en consecuencia, sin nuestra intervención.
Ahora, necesitamos implementar el código del lado del servicio. En una aplicación del mundo real, estaría actualizando manualmente la aplicación después de actualizar su fuente para reflejar todos los cambios de diseño. Esta es la forma en que Goa recomienda que procedamos, pero por brevedad, eliminaré y volveré a generar la aplicación de ejemplo para llegar más rápido. Ejecute los siguientes comandos para eliminar la aplicación de ejemplo y volver a generarla:
rm -rf cmd client.go goa example clients/design
Con eso, su código debería verse como el siguiente:
Podemos ver un archivo nuevo en nuestra aplicación de ejemplo: signin.go
, que contiene la lógica del servicio de inicio de sesión. Sin embargo, podemos ver que client.go
también se actualizó con una función JWTAuth para validar tokens. Esto coincide con lo que hemos escrito en el diseño, por lo que cada llamada a cualquier método en el cliente se interceptará para la validación del token y se reenviará solo si está autorizado por un token válido y un alcance correcto.
Por lo tanto, actualizaremos los métodos en nuestro Servicio de inicio de sesión dentro de signin.go
para agregar la lógica para generar los tokens que la API creará para los usuarios autenticados. Copie y pegue el siguiente contexto en 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 }
Finalmente, debido a que agregamos más campos a nuestro tipo personalizado, necesitamos actualizar el método Add en el servicio del cliente en client.go
para reflejar dichos cambios. Copie y pegue lo siguiente para actualizar su 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 }
¡Y eso es! Vamos a volver a compilar la aplicación y probarla de nuevo. Ejecute los siguientes comandos para eliminar los archivos binarios antiguos y compilar nuevos:
rm -f clients clients-cli go build ./cmd/clients go build ./cmd/clients-cli
Ejecute ./clients
nuevamente y déjelo en ejecución. Debería verlo funcionando correctamente, pero esta vez, con los nuevos 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 probar, ejecutemos todos los métodos de la API mediante la CLI. Tenga en cuenta que estamos utilizando las credenciales 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 } ]
¡Y allá vamos! Tenemos una aplicación minimalista con autenticación adecuada, autorización de alcance y espacio para el crecimiento evolutivo. Después de esto, puede desarrollar su propia estrategia de autenticación utilizando servicios en la nube o cualquier otro proveedor de identidad de su elección. También puede crear complementos para su base de datos o sistema de mensajería preferido, o incluso integrarlos fácilmente con otras API.
Consulte el proyecto GitHub de Goa para obtener más complementos, ejemplos (que muestran capacidades específicas del marco) y otros recursos útiles.
Es todo por hoy. Espero que hayas disfrutado jugando con Goa y leyendo este artículo. Si tiene algún comentario sobre el contenido, no dude en comunicarse con GitHub, Twitter o LinkedIn.
Además, pasamos el rato en el canal #goa en Gophers Slack, ¡así que ven y saluda!
Para obtener más información sobre Golang, consulte Lenguaje de programación Go: un tutorial introductorio de Golang y Lógica bien estructurada: un tutorial de programación orientada a objetos de Golang.