Sviluppo API in Go utilizzando Goa
Pubblicato: 2022-03-11Lo sviluppo di API è un argomento caldo al giorno d'oggi. Esistono moltissimi modi in cui puoi sviluppare e fornire un'API e le grandi aziende hanno sviluppato soluzioni enormi per aiutarti a avviare rapidamente un'applicazione.
Tuttavia, la maggior parte di queste opzioni manca di una caratteristica chiave: la gestione del ciclo di vita dello sviluppo. Quindi, gli sviluppatori passano alcuni cicli a creare API utili e robuste, ma finiscono per lottare con l'evoluzione organica prevista del loro codice e le implicazioni che un piccolo cambiamento nell'API ha nel codice sorgente.
Nel 2016, Raphael Simon ha creato Goa, un framework per lo sviluppo di API in Golang con un ciclo di vita che mette la progettazione API al primo posto. In Goa, la definizione dell'API non è solo descritta come codice, ma è anche l'origine da cui derivano il codice del server, il codice client e la documentazione. Ciò significa che il codice è descritto nella definizione API utilizzando un Golang Domain Specific Language (DSL), quindi generato utilizzando goa cli e implementato separatamente dal codice sorgente dell'applicazione.
Questo è il motivo per cui Goa brilla. È una soluzione con un contratto del ciclo di vita di sviluppo ben definito che si basa sulle migliori pratiche durante la generazione del codice (come la suddivisione di domini e preoccupazioni diversi in livelli, in modo che gli aspetti di trasporto non interferiscano con gli aspetti aziendali dell'applicazione), seguendo un modello di architettura pulito dove componibile i moduli vengono generati per i livelli di trasporto, endpoint e logica aziendale nell'applicazione.
Alcune funzionalità di Goa, come definite dal sito ufficiale, includono:
- Componibilità . Il pacchetto, gli algoritmi di generazione del codice e il codice generato sono tutti modulari.
- Indipendente dal trasporto . Il disaccoppiamento del livello di trasporto dall'effettiva implementazione del servizio significa che lo stesso servizio può esporre endpoint accessibili tramite più trasporti come HTTP e/o gRPC.
- Separazione delle preoccupazioni . L'effettiva implementazione del servizio è isolata dal codice di trasporto.
- Utilizzo dei tipi di libreria standard Go . Ciò semplifica l'interfaccia con il codice esterno.
In questo articolo creerò un'applicazione e ti guiderò attraverso le fasi del ciclo di vita di sviluppo dell'API. L'applicazione gestisce i dettagli sui clienti, come nome, indirizzo, numero di telefono, social media, ecc. Alla fine, cercheremo di estenderlo e aggiungere nuove funzionalità per esercitare il suo ciclo di vita di sviluppo.
Quindi iniziamo!
Preparare la tua area di sviluppo
Il nostro primo passo è avviare il repository e abilitare il supporto dei moduli Go:
mkdir -p clients/design cd clients go mod init clients
Alla fine, la struttura del tuo repository dovrebbe essere come di seguito:
$ tree . ├── design └── go.mod
Progettare la tua API
La fonte di verità per la tua API è la definizione del tuo progetto. Come afferma la documentazione, "Goa ti consente di pensare alle tue API indipendentemente da qualsiasi problema di implementazione e quindi rivedere quel progetto con tutte le parti interessate prima di scrivere l'implementazione". Ciò significa che ogni elemento dell'API viene definito qui prima, prima che venga generato il codice dell'applicazione effettivo. Ma basta parlare!
Apri il file clients/design/design.go
e aggiungi il contenuto qui sotto:
/* 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 prima cosa che puoi notare è che la DSL sopra è un insieme di funzioni Go che possono essere composte per descrivere un'API di servizio remoto. Le funzioni sono composte utilizzando argomenti di funzione anonimi. Nelle funzioni DSL, abbiamo un sottoinsieme di funzioni che non dovrebbero apparire all'interno di altre funzioni, che chiamiamo DSL di livello superiore. Di seguito, hai un insieme parziale delle funzioni DSL e la loro struttura:
Quindi, nella nostra progettazione iniziale abbiamo un DSL di livello superiore dell'API che descrive l'API del nostro cliente, un DSL di livello superiore del servizio che descrive il servizio API principale, clients
e serve il file swagger dell'API e due DSL di livello superiore di tipo per descrivere il tipo di vista oggetto utilizzato nel carico utile di trasporto.
La funzione API
è un DSL di primo livello opzionale che elenca le proprietà globali dell'API come un nome, una descrizione e anche uno o più server che potenzialmente espongono diversi insiemi di servizi. Nel nostro caso, un server è sufficiente, ma potresti anche servire diversi servizi in diversi livelli: sviluppo, test e produzione, ad esempio.
La funzione Service
definisce un gruppo di metodi che potenzialmente esegue il mapping a una risorsa nel trasporto. Un servizio può anche definire risposte di errore comuni. I metodi di servizio sono descritti utilizzando Method
. Questa funzione definisce i tipi di payload del metodo (input) e di risultato (output). Se ometti il payload o il tipo di risultato, viene utilizzato il tipo predefinito Empty, che esegue il mapping a un corpo vuoto in HTTP.
Infine, le funzioni Type
o ResultType
definiscono tipi definiti dall'utente, la differenza principale è che un tipo di risultato definisce anche un insieme di "viste".
Nel nostro esempio, abbiamo descritto l'API e spiegato come dovrebbe funzionare, e abbiamo anche creato quanto segue:
- Un servizio chiamato
clients
- Tre metodi:
add
(per creare un client),get
(per recuperare un client) eshow
(per elencare tutti i client) - I nostri tipi personalizzati, che torneranno utili quando integriamo con un database, e un tipo di errore personalizzato
Ora che la nostra applicazione è stata descritta, possiamo generare il codice boilerplate. Il comando seguente prende il percorso di importazione del pacchetto di progettazione come argomento. Accetta anche il percorso della directory di output come flag opzionale:
goa gen clients/design
Il comando restituisce i nomi dei file che genera. Al suo interno, la directory gen
contiene la sottodirectory del nome dell'applicazione che ospita il codice del servizio indipendente dal trasporto. La sottodirectory http
descrive il trasporto HTTP (abbiamo il codice server e client con la logica per codificare e decodificare richieste e risposte e il codice CLI per creare richieste HTTP dalla riga di comando). Contiene anche i file delle specifiche Open API 2.0 nei formati JSON e YAML.
Puoi copiare il contenuto di un file swagger e incollarlo su qualsiasi editor online Swagger (come quello su swagger.io) per visualizzare la documentazione delle specifiche API. Supportano entrambi i formati YAML e JSON.
Ora siamo pronti per il nostro prossimo passo nel ciclo di vita dello sviluppo.
Implementazione della tua API
Dopo aver creato il codice standard, è il momento di aggiungere un po' di logica aziendale. A questo punto, ecco come dovrebbe apparire il tuo codice:
Dove ogni file sopra viene mantenuto e aggiornato da Goa ogni volta che eseguiamo la CLI. Pertanto, man mano che l'architettura si evolve, il tuo design seguirà l'evoluzione, così come il tuo codice sorgente. Per implementare l'applicazione, eseguiamo il comando seguente (genererà un'implementazione di base del servizio insieme a file server compilabili che avviano goroutine per avviare un server HTTP e file client che possono inviare richieste a quel server):
goa example clients/design
Questo genererà una cartella cmd con origini compilabili sia dal server che dal client. Ci sarà la tua applicazione e quelli sono i file che dovresti mantenere dopo che Goa li ha generati per la prima volta.
La documentazione di Goa chiarisce che: "Questo comando genera un punto di partenza per il servizio per aiutare lo sviluppo di bootstrap, in particolare NON è pensato per essere eseguito nuovamente quando il design cambia".
Ora, il tuo codice sarà simile a:
Dove client.go
è un file di esempio con un'implementazione fittizia di entrambi i metodi get
e show
. Aggiungiamo un po' di logica di business!
Per semplicità, useremo SQLite invece di un database in memoria e Gorm come nostro ORM. Crea il file sqlite.go
e aggiungi il contenuto di seguito, che aggiungerà la logica del database per creare record sul database ed elencare una e/o più righe dal database:
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 }
Quindi, modifichiamo client.go per aggiornare tutti i metodi nel servizio client, implementando le chiamate al database e costruendo le risposte 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 }
Il primo taglio della nostra applicazione è pronto per essere compilato. Eseguire il comando seguente per creare file binari server e client:
go build ./cmd/clients go build ./cmd/clients-cli
Per eseguire il server, esegui semplicemente ./clients
. Lascialo in esecuzione per ora. Dovresti vederlo funzionare correttamente, come il seguente:
$ ./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"
Siamo pronti per eseguire alcuni test nella nostra applicazione. Proviamo tutti i metodi usando il 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 si verifica un errore, controllare i registri del server per assicurarsi che la logica ORM di SQLite sia corretta e che non si verifichino errori del database come database non inizializzato o query che non restituiscono righe.
Estendere la tua API
Il framework supporta lo sviluppo di plugin per estendere la tua API e aggiungere più funzionalità facilmente. Goa ha un repository per i plugin creati dalla community.
Come spiegato in precedenza, come parte del ciclo di vita dello sviluppo, possiamo fare affidamento sul set di strumenti per estendere la nostra applicazione tornando alla definizione del progetto, aggiornandola e aggiornando il codice generato. Mostriamo come i plugin possono aiutare aggiungendo CORS e autenticazione all'API.
Aggiorna il file clients/design/design.go
al contenuto seguente:
/* 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) }) }) })
Puoi notare due differenze principali nel nuovo design. Abbiamo definito un ambito di sicurezza nel servizio client
in modo da poter convalidare se un utente è autorizzato a invocare il servizio e abbiamo definito un secondo servizio chiamato signin
, che utilizzeremo per autenticare gli utenti e generare JSON Web Tokens (JWT), che il il servizio client
utilizzerà per autorizzare le chiamate. Abbiamo anche aggiunto più campi al nostro tipo di client personalizzato. Questo è un caso comune nello sviluppo di un'API: la necessità di rimodellare o ristrutturare i dati.

Sulla progettazione, queste modifiche possono sembrare semplici, ma riflettendo su di esse, sono necessarie molte funzionalità minime per ottenere ciò che è descritto nella progettazione. Prendi, ad esempio, gli schemi architetturali per l'autenticazione e l'autorizzazione utilizzando i nostri metodi API:
Queste sono tutte nuove funzionalità che il nostro codice non ha ancora. Ancora una volta, è qui che Goa aggiunge più valore ai tuoi sforzi di sviluppo. Implementiamo queste funzionalità lato trasporto rigenerando nuovamente il codice sorgente con il comando seguente:
goa gen clients/design
A questo punto, se stai usando Git, noterai la presenza di nuovi file, con altri che verranno visualizzati come aggiornati. Questo perché Goa ha aggiornato senza problemi il codice standard di conseguenza, senza il nostro intervento.
Ora, dobbiamo implementare il codice lato servizio. In un'applicazione reale, aggiorneresti manualmente l'applicazione dopo aver aggiornato la tua fonte per riflettere tutte le modifiche di progettazione. Questo è il modo in cui Goa consiglia di procedere, ma per brevità eliminerò e rigenererò l'applicazione di esempio per arrivarci più velocemente. Eseguire i comandi seguenti per eliminare l'applicazione di esempio e rigenerarla:
rm -rf cmd client.go goa example clients/design
Con ciò, il tuo codice dovrebbe essere simile al seguente:
Possiamo vedere un nuovo file nella nostra applicazione di esempio: signin.go
, che contiene la logica del servizio di accesso. Tuttavia, possiamo vedere che client.go
è stato aggiornato anche con una funzione JWTAuth per la convalida dei token. Questo corrisponde a quanto abbiamo scritto nella progettazione, quindi ogni chiamata a qualsiasi metodo nel client verrà intercettata per la convalida del token e inoltrata solo se autorizzata da un token valido e da un ambito corretto.
Pertanto, aggiorneremo i metodi nel nostro servizio di accesso all'interno di signin.go
per aggiungere la logica per generare i token che l'API creerà per gli utenti autenticati. Copia e incolla il seguente contesto in 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 }
Infine, poiché abbiamo aggiunto più campi al nostro tipo personalizzato, è necessario aggiornare il metodo Aggiungi sul servizio client in client.go
per riflettere tali modifiche. Copia e incolla quanto segue per aggiornare il tuo 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 questo è tutto! Ricompiliamo l'applicazione e la testiamo di nuovo. Esegui i comandi seguenti per rimuovere i vecchi binari e compilarne di nuovi:
rm -f clients clients-cli go build ./cmd/clients go build ./cmd/clients-cli
Esegui di nuovo ./clients
e lascialo in esecuzione. Dovresti vederlo funzionare correttamente, ma questa volta, con i nuovi metodi implementati:
$ ./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"
Per testare, eseguiamo tutti i metodi API usando il cli: si noti che stiamo usando le credenziali hardcoded:
$ ./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 ci siamo! Abbiamo un'applicazione minimalista con autenticazione adeguata, autorizzazione dell'ambito e spazio per la crescita evolutiva. Successivamente, puoi sviluppare la tua strategia di autenticazione utilizzando i servizi cloud o qualsiasi altro provider di identità di tua scelta. Puoi anche creare plug-in per il tuo database o sistema di messaggistica preferito o persino integrarti facilmente con altre API.
Dai un'occhiata al progetto GitHub di Goa per ulteriori plug-in, esempi (che mostrano funzionalità specifiche del framework) e altre risorse utili.
Questo è tutto per oggi. Spero che ti sia piaciuto giocare con Goa e leggere questo articolo. Se hai commenti sui contenuti, non esitare a contattarci su GitHub, Twitter o LinkedIn.
Inoltre, usciamo nel canale #goa su Gophers Slack, quindi vieni a salutarci!
Per ulteriori informazioni su Golang, vedere Go Programming Language: An Introductory Golang Tutorial e Well-structured Logic: A Golang OOP Tutorial.