Développement d'API dans Go à l'aide de Goa

Publié: 2022-03-11

Le développement d'API est un sujet brûlant de nos jours. Il existe un grand nombre de façons de développer et de fournir une API, et les grandes entreprises ont développé des solutions massives pour vous aider à démarrer rapidement une application.

Pourtant, la plupart de ces options manquent d'une fonctionnalité clé : la gestion du cycle de vie du développement. Ainsi, les développeurs passent quelques cycles à créer des API utiles et robustes, mais finissent par se débattre avec l'évolution organique attendue de leur code et les implications qu'un petit changement de l'API a sur le code source.

En 2016, Raphael Simon a créé Goa, un framework pour le développement d'API dans Golang avec un cycle de vie qui place la conception d'API au premier plan. Dans Goa, votre définition d'API n'est pas seulement décrite comme du code, mais est également la source à partir de laquelle le code serveur, le code client et la documentation sont dérivés. Cela signifie que votre code est décrit dans votre définition d'API à l'aide d'un langage spécifique au domaine (DSL) Golang, puis généré à l'aide de l'interface de ligne de commande goa et implémenté séparément du code source de votre application.

C'est la raison pour laquelle Goa brille. C'est une solution avec un contrat de cycle de vie de développement bien défini qui s'appuie sur les meilleures pratiques lors de la génération de code (comme le fractionnement de différents domaines et préoccupations en couches, afin que les aspects de transport n'interfèrent pas avec les aspects commerciaux de l'application), suivant un modèle d'architecture propre où composable des modules sont générés pour les couches de transport, de point de terminaison et de logique métier dans votre application.

Certaines fonctionnalités de Goa, telles que définies par le site officiel, incluent :

  • Composabilité . Le package, les algorithmes de génération de code et le code généré sont tous modulaires.
  • Transport-agnostique . Le découplage de la couche de transport de l'implémentation réelle du service signifie que le même service peut exposer des points de terminaison accessibles via plusieurs transports tels que HTTP et/ou gRPC.
  • Séparation des préoccupations . La mise en œuvre effective du service est isolée du code de transport.
  • Utilisation des types de bibliothèque standard Go . Cela facilite l'interface avec le code externe.

Dans cet article, je vais créer une application et vous guider à travers les étapes du cycle de vie du développement d'une API. L'application gère les détails des clients, tels que le nom, l'adresse, le numéro de téléphone, les médias sociaux, etc. En fin de compte, nous tenterons de l'étendre et d'ajouter de nouvelles fonctionnalités pour exercer son cycle de vie de développement.

Alors, commençons!

Préparer votre zone de développement

Notre première étape consiste à lancer le référentiel et à activer la prise en charge des modules Go :

 mkdir -p clients/design cd clients go mod init clients

Au final, votre structure de dépôt devrait être comme ci-dessous :

 $ tree . ├── design └── go.mod

Conception de votre API

La source de vérité pour votre API est votre définition de conception. Comme l'indique la documentation, "Goa vous permet de réfléchir à vos API indépendamment de tout problème d'implémentation, puis d'examiner cette conception avec toutes les parties prenantes avant d'écrire l'implémentation". Cela signifie que chaque élément de l'API est défini ici en premier, avant que le code d'application réel ne soit généré. Mais assez parlé !

Ouvrez le fichier clients/design/design.go et ajoutez le contenu ci-dessous :

 /* 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") })

La première chose que vous pouvez remarquer est que le DSL ci-dessus est un ensemble de fonctions Go qui peuvent être composées pour décrire une API de service distant. Les fonctions sont composées à l'aide d'arguments de fonction anonymes. Dans les fonctions DSL, nous avons un sous-ensemble de fonctions qui ne sont pas censées apparaître dans d'autres fonctions, que nous appelons DSL de niveau supérieur. Ci-dessous, vous avez un ensemble partiel de fonctions DSL et leur structure :

Fonctions ADSL

Ainsi, nous avons dans notre conception initiale un DSL de haut niveau d'API décrivant l'API de notre client, un DSL de haut niveau de service décrivant le service d'API principal, clients et servant le fichier swagger d'API, et deux types de DSL de haut niveau pour décrire le type de vue d'objet utilisé dans la charge utile de transport.

La fonction API est un DSL de niveau supérieur facultatif qui répertorie les propriétés globales de l'API telles qu'un nom, une description, ainsi qu'un ou plusieurs serveurs exposant potentiellement différents ensembles de services. Dans notre cas, un serveur suffit, mais vous pouvez également proposer différents services à différents niveaux : développement, test et production, par exemple.

La fonction Service définit un groupe de méthodes qui sont potentiellement mappées à une ressource dans le transport. Un service peut également définir des réponses d'erreur courantes. Les méthodes de service sont décrites à l'aide de Method . Cette fonction définit les types de charge utile (entrée) et de résultat (sortie) de la méthode. Si vous omettez la charge utile ou le type de résultat, le type intégré Empty, qui correspond à un corps vide dans HTTP, est utilisé.

Enfin, les fonctions Type ou ResultType définissent des types définis par l'utilisateur, la principale différence étant qu'un type de résultat définit également un ensemble de "vues".

Dans notre exemple, nous avons décrit l'API et expliqué comment elle devrait servir, et nous avons également créé ce qui suit :

  • Un service appelé clients
  • Trois méthodes : add (pour créer un client), get (pour récupérer un client) et show (pour répertorier tous les clients)
  • Nos propres types personnalisés, qui seront utiles lors de l'intégration à une base de données, et un type d'erreur personnalisé

Maintenant que notre application a été décrite, nous pouvons générer le code passe-partout. La commande suivante prend le chemin d'importation du package de conception comme argument. Il accepte également le chemin d'accès au répertoire de sortie comme indicateur facultatif :

 goa gen clients/design

La commande affiche les noms des fichiers qu'elle génère. Dans celui-ci, le répertoire gen contient le sous-répertoire du nom de l'application qui héberge le code de service indépendant du transport. Le sous-répertoire http décrit le transport HTTP (nous avons le code serveur et client avec la logique pour encoder et décoder les requêtes et les réponses, et le code CLI pour créer des requêtes HTTP à partir de la ligne de commande). Il contient également les fichiers de spécification Open API 2.0 aux formats JSON et YAML.

Vous pouvez copier le contenu d'un fichier swagger et le coller dans n'importe quel éditeur Swagger en ligne (comme celui de swagger.io) pour visualiser la documentation de vos spécifications d'API. Ils prennent en charge les formats YAML et JSON.

Nous sommes maintenant prêts pour notre prochaine étape du cycle de développement.

Implémentation de votre API

Une fois votre code passe-partout créé, il est temps d'y ajouter une logique métier. À ce stade, voici à quoi votre code devrait ressembler :

Développement d'API en Go

Où chaque fichier ci-dessus est maintenu et mis à jour par Goa chaque fois que nous exécutons la CLI. Ainsi, au fur et à mesure que l'architecture évolue, votre conception suivra l'évolution, et votre code source aussi. Pour implémenter l'application, nous exécutons la commande ci-dessous (elle générera une implémentation de base du service ainsi que des fichiers de serveur à construire qui font tourner des goroutines pour démarrer un serveur HTTP et des fichiers client qui peuvent envoyer des requêtes à ce serveur) :

 goa example clients/design

Cela générera un dossier cmd avec les sources de construction du serveur et du client. Il y aura votre application, et ce sont les fichiers que vous devez conserver après que Goa les ait générés pour la première fois.

La documentation de Goa indique clairement que : "Cette commande génère un point de départ pour le service afin d'aider au démarrage du développement - en particulier, elle n'est PAS destinée à être réexécutée lorsque la conception change."

Maintenant, votre code ressemblera à :

Développement d'API en Go : dossier cmd

client.go est un exemple de fichier avec une implémentation factice des méthodes get et show . Ajoutons-y une logique métier !

Pour plus de simplicité, nous utiliserons SQLite au lieu d'une base de données en mémoire et Gorm comme ORM. Créez le fichier sqlite.go et ajoutez le contenu ci-dessous - qui ajoutera une logique de base de données pour créer des enregistrements sur la base de données et lister une et/ou plusieurs lignes de la base de données :

 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 }

Ensuite, nous éditons client.go pour mettre à jour toutes les méthodes dans le service client, en implémentant les appels de base de données et en construisant les réponses de l'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 }

La première version de notre application est prête à être compilée. Exécutez la commande suivante pour créer des fichiers binaires serveur et client :

 go build ./cmd/clients go build ./cmd/clients-cli

Pour exécuter le serveur, exécutez simplement ./clients . Laissez-le fonctionner pour le moment. Vous devriez le voir fonctionner correctement, comme suit :

 $ ./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"

Nous sommes prêts à effectuer des tests dans notre application. Essayons toutes les méthodes en utilisant le 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 vous obtenez une erreur, vérifiez les journaux du serveur pour vous assurer que la logique SQLite ORM est bonne et que vous ne rencontrez aucune erreur de base de données, telle qu'une base de données non initialisée ou des requêtes ne renvoyant aucune ligne.

Extension de votre API

Le framework prend en charge le développement de plugins pour étendre votre API et ajouter plus de fonctionnalités facilement. Goa dispose d'un référentiel pour les plugins créés par la communauté.

Comme je l'ai expliqué précédemment, dans le cadre du cycle de développement, nous pouvons compter sur l'ensemble d'outils pour étendre notre application en revenant à la définition de conception, en la mettant à jour et en actualisant notre code généré. Montrons comment les plugins peuvent aider en ajoutant CORS et l'authentification à l'API.

Mettez à jour le fichier clients/design/design.go avec le contenu ci-dessous :

 /* 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) }) }) })

Vous pouvez remarquer deux différences majeures dans le nouveau design. Nous avons défini une étendue de sécurité dans le service client afin de pouvoir valider si un utilisateur est autorisé à invoquer le service, et nous avons défini un deuxième service appelé signin , que nous utiliserons pour authentifier les utilisateurs et générer des jetons Web JSON (JWT), que le service client utilisera pour autoriser les appels. Nous avons également ajouté plus de champs à notre type de client personnalisé. Il s'agit d'un cas courant lors du développement d'une API : la nécessité de remodeler ou de restructurer les données.

En ce qui concerne la conception, ces changements peuvent sembler simples, mais en y réfléchissant, de nombreuses fonctionnalités minimales sont nécessaires pour réaliser ce qui est décrit sur la conception. Prenons, par exemple, les schémas architecturaux pour l'authentification et l'autorisation à l'aide de nos méthodes API :

Développement d'API en Go : schémas architecturaux pour l'authentification et l'autorisation

Ce sont toutes de nouvelles fonctionnalités que notre code n'a pas encore. Encore une fois, c'est là que Goa ajoute plus de valeur à vos efforts de développement. Implémentons ces fonctionnalités côté transport en régénérant à nouveau le code source avec la commande ci-dessous :

 goa gen clients/design

À ce stade, si vous utilisez Git, vous remarquerez la présence de nouveaux fichiers, d'autres s'affichant comme mis à jour. En effet, Goa a actualisé de manière transparente le code passe-partout en conséquence, sans notre intervention.

Maintenant, nous devons implémenter le code côté service. Dans une application réelle, vous mettriez à jour manuellement l'application après avoir mis à jour votre source pour refléter toutes les modifications de conception. C'est ainsi que Goa nous recommande de procéder, mais par souci de brièveté, je vais supprimer et régénérer l'exemple d'application pour nous y rendre plus rapidement. Exécutez les commandes ci-dessous pour supprimer l'exemple d'application et le régénérer :

 rm -rf cmd client.go goa example clients/design

Avec cela, votre code devrait ressembler à ceci :

Développement d'API en Go : régénérer

Nous pouvons voir un nouveau fichier dans notre exemple d'application : signin.go , qui contient la logique du service de connexion. Cependant, nous pouvons voir que client.go a également été mis à jour avec une fonction JWTAuth pour valider les jetons. Cela correspond à ce que nous avons écrit dans la conception, de sorte que chaque appel à n'importe quelle méthode dans le client sera intercepté pour la validation du jeton et transmis uniquement s'il est autorisé par un jeton valide et une portée correcte.

Par conséquent, nous mettrons à jour les méthodes de notre service de connexion dans signin.go afin d'ajouter la logique pour générer les jetons que l'API créera pour les utilisateurs authentifiés. Copiez et collez le contexte suivant dans 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 }

Enfin, comme nous avons ajouté plus de champs à notre type personnalisé, nous devons mettre à jour la méthode Add sur le service client dans client.go pour refléter ces modifications. Copiez et collez ce qui suit pour mettre à jour votre 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 }

Et c'est tout! Recompilons l'application et testons-la à nouveau. Exécutez les commandes ci-dessous pour supprimer les anciens fichiers binaires et en compiler de nouveaux :

 rm -f clients clients-cli go build ./cmd/clients go build ./cmd/clients-cli

Exécutez à nouveau ./clients et laissez-le fonctionner. Vous devriez le voir fonctionner avec succès, mais cette fois, avec les nouvelles méthodes implémentées :

 $ ./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"

Pour tester, exécutons toutes les méthodes de l'API à l'aide de la cli. Notez que nous utilisons les informations d'identification codées en dur :

 $ ./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 } ]

Et voilà ! Nous avons une application minimaliste avec une authentification appropriée, une autorisation de portée et une marge de croissance évolutive. Après cela, vous pourrez développer votre propre stratégie d'authentification à l'aide de services cloud ou de tout autre fournisseur d'identité de votre choix. Vous pouvez également créer des plugins pour votre base de données ou votre système de messagerie préféré, ou même les intégrer facilement à d'autres API.

Consultez le projet GitHub de Goa pour plus de plugins, d'exemples (montrant les capacités spécifiques du framework) et d'autres ressources utiles.

C'est tout pour aujourd'hui. J'espère que vous avez aimé jouer avec Goa et lire cet article. Si vous avez des commentaires sur le contenu, n'hésitez pas à nous contacter sur GitHub, Twitter ou LinkedIn.

De plus, nous traînons sur la chaîne #goa sur Gophers Slack, alors venez nous dire bonjour !

Pour plus d'informations sur Golang, consultez Go Langage de programmation : un didacticiel d'introduction à Golang et Logique bien structurée : un didacticiel Golang OOP.