Dezvoltare API în Go folosind Goa

Publicat: 2022-03-11

Dezvoltarea API este un subiect fierbinte în zilele noastre. Există un număr imens de moduri în care puteți dezvolta și furniza un API, iar companiile mari au dezvoltat soluții masive pentru a vă ajuta să porniți rapid o aplicație.

Cu toate acestea, celor mai multe dintre aceste opțiuni le lipsește o caracteristică cheie: managementul ciclului de viață al dezvoltării. Deci, dezvoltatorii petrec câteva cicluri creând API-uri utile și robuste, dar ajung să se lupte cu evoluția organică așteptată a codului lor și cu implicațiile pe care o mică schimbare a API-ului le are în codul sursă.

În 2016, Raphael Simon a creat Goa, un cadru pentru dezvoltarea API în Golang, cu un ciclu de viață care pune designul API pe primul loc. În Goa, definiția dvs. API nu este descrisă doar ca cod, ci este și sursa din care sunt derivate codul serverului, codul client și documentația. Aceasta înseamnă că codul dvs. este descris în definiția dvs. API folosind un Golang Domain Specific Language (DSL), apoi generat folosind goa cli și implementat separat de codul sursă al aplicației.

Acesta este motivul pentru care Goa strălucește. Este o soluție cu un contract de ciclu de viață de dezvoltare bine definit, care se bazează pe cele mai bune practici la generarea codului (cum ar fi împărțirea diferitelor domenii și preocupări în straturi, astfel încât aspectele de transport să nu interfereze cu aspectele de afaceri ale aplicației), urmând un model de arhitectură curat acolo unde este compus. modulele sunt generate pentru straturile de transport, punct final și logica de afaceri din aplicația dvs.

Unele caracteristici Goa, așa cum sunt definite de site-ul oficial, includ:

  • Composabilitate . Pachetul, algoritmii de generare a codului și codul generat sunt toate modulare.
  • Transport-agnostic . Decuplarea stratului de transport de implementarea efectivă a serviciului înseamnă că același serviciu poate expune punctele finale accesibile prin mai multe transporturi, cum ar fi HTTP și/sau gRPC.
  • Separarea preocupărilor . Implementarea efectivă a serviciului este izolată de codul de transport.
  • Utilizarea tipurilor de biblioteci standard Go . Acest lucru facilitează interfața cu codul extern.

În acest articol, voi crea o aplicație și vă voi ghida prin etapele ciclului de viață al dezvoltării API. Aplicația gestionează detalii despre clienți, cum ar fi numele, adresa, numărul de telefon, social media etc. În final, vom încerca să o extindem și să adăugăm noi funcții pentru a-și exercita ciclul de dezvoltare.

Asadar, haideti sa începem!

Pregătirea zonei de dezvoltare

Primul nostru pas este să inițiem depozitul și să activăm suportul pentru module Go:

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

În cele din urmă, structura dvs. de repo ar trebui să fie ca mai jos:

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

Proiectarea API-ului dvs

Sursa adevărului pentru API-ul dvs. este definiția dvs. de design. După cum se arată în documentație, „Goa vă permite să vă gândiți la API-urile dvs. independent de orice problemă de implementare și apoi să revizuiți designul cu toate părțile interesate înainte de a scrie implementarea.” Aceasta înseamnă că fiecare element al API-ului este definit aici mai întâi, înainte ca codul aplicației efectiv să fie generat. Dar destul de vorbit!

Deschideți fișierul clients/design/design.go și adăugați conținutul de mai jos:

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

Primul lucru pe care îl puteți observa este că DSL-ul de mai sus este un set de funcții Go care pot fi compuse pentru a descrie un API de serviciu la distanță. Funcțiile sunt compuse folosind argumente de funcție anonime. În funcțiile DSL, avem un subset de funcții care nu ar trebui să apară în alte funcții, pe care le numim DSL-uri de nivel superior. Mai jos, aveți un set parțial de funcții DSL și structura lor:

Funcții DSL

Deci, avem în proiectarea noastră inițială un DSL de nivel superior API care descrie API-ul clientului nostru, un DSL de nivel superior al unui serviciu care descrie serviciul API principal, clients și care deservesc fișierul API swagger și două tipuri DSL-uri de nivel superior pentru descrierea tip de vizualizare a obiectului utilizat în sarcina utilă de transport.

Funcția API este un DSL opțional de nivel superior care listează proprietățile globale ale API-ului, cum ar fi un nume, o descriere și, de asemenea, unul sau mai multe servere care pot expune diferite seturi de servicii. În cazul nostru, un server este suficient, dar puteți servi și diferite servicii în diferite niveluri: dezvoltare, testare și producție, de exemplu.

Funcția Service definește un grup de metode care pot fi mapate la o resursă din transport. Un serviciu poate defini, de asemenea, răspunsuri de eroare comune. Metodele de service sunt descrise folosind Method . Această funcție definește tipurile de sarcină utilă a metodei (intrare) și rezultat (ieșire). Dacă omiteți sarcina utilă sau tipul de rezultat, este utilizat tipul încorporat Gol, care se mapează la un corp gol în HTTP.

În cele din urmă, funcțiile Type sau ResultType definesc tipuri definite de utilizator, principala diferență fiind că un tip de rezultat definește și un set de „vizualizări”.

În exemplul nostru, am descris API-ul și am explicat cum ar trebui să servească și am creat, de asemenea, următoarele:

  • Un serviciu numit clients
  • Trei metode: add (pentru crearea unui client), get (pentru preluarea unui client) și show (pentru listarea tuturor clienților)
  • Tipurile noastre personalizate, care vor fi utile atunci când integrăm o bază de date și un tip de eroare personalizat

Acum că aplicația noastră a fost descrisă, putem genera codul standard. Următoarea comandă ia ca argument calea de import a pachetului de design. De asemenea, acceptă calea către directorul de ieșire ca flag opțional:

 goa gen clients/design

Comanda afișează numele fișierelor pe care le generează. Acolo, directorul gen conține subdirectorul cu numele aplicației care găzduiește codul de serviciu independent de transport. Subdirectorul http descrie transportul HTTP (avem cod de server și client cu logica pentru a codifica și decodifica cererile și răspunsurile, iar codul CLI pentru a construi cereri HTTP din linia de comandă). De asemenea, conține fișierele de specificații Open API 2.0 în ambele formate JSON și YAML.

Puteți copia conținutul unui fișier Swagger și îl puteți lipi în orice editor online Swagger (cum ar fi cel de la swagger.io) pentru a vizualiza documentația cu specificațiile API. Aceștia acceptă atât formatele YAML, cât și formatele JSON.

Acum suntem pregătiți pentru următorul nostru pas în ciclul de viață al dezvoltării.

Implementarea API-ului dvs

După ce a fost creat codul standard, este timpul să îi adăugați o logică de afaceri. În acest moment, așa ar trebui să arate codul dvs.:

Dezvoltare API în Go

Unde fiecare fișier de mai sus este menținut și actualizat de Goa ori de câte ori executăm CLI. Astfel, pe măsură ce arhitectura evoluează, designul tău va urma evoluția, la fel și codul tău sursă. Pentru a implementa aplicația, executăm comanda de mai jos (va genera o implementare de bază a serviciului împreună cu fișierele server care se pot construi, care pornesc goroutine pentru a porni un server HTTP și fișiere client care pot face cereri către acel server):

 goa example clients/design

Acest lucru va genera un folder cmd cu surse de compilare atât pentru server, cât și pentru client. Va exista aplicația dvs. și acestea sunt fișierele pe care ar trebui să le întrețineți după ce Goa le generează pentru prima dată.

Documentația Goa arată clar că: „Această comandă generează un punct de plecare pentru serviciu pentru a ajuta la dezvoltarea bootstrap - în special, NU este menită să fie reluată atunci când designul se schimbă.”

Acum, codul tău va arăta astfel:

Dezvoltare API în Go: folder cmd

Unde client.go este un exemplu de fișier cu o implementare inactivă a metodelor get și show . Să adăugăm ceva logică de afaceri!

Pentru simplitate, vom folosi SQLite în loc de o bază de date în memorie și Gorm ca ORM. Creați fișierul sqlite.go și adăugați conținutul de mai jos - care va adăuga logica bazei de date pentru a crea înregistrări în baza de date și va lista unul și/sau mai multe rânduri din baza de date:

 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 }

Apoi, edităm client.go pentru a actualiza toate metodele din Client Service, implementând apelurile la baza de date și construind răspunsurile 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 }

Prima tăietură a aplicației noastre este gata pentru a fi compilată. Rulați următoarea comandă pentru a crea fișiere binare pentru server și client:

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

Pentru a rula serverul, trebuie doar să rulați ./clients . Lasă-l să funcționeze deocamdată. Ar trebui să vedeți că funcționează cu succes, după cum urmează:

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

Suntem gata să efectuăm câteva teste în aplicația noastră. Să încercăm toate metodele folosind 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" } ]

Dacă primiți vreo eroare, verificați jurnalele serverului pentru a vă asigura că logica SQLite ORM este bună și că nu vă confruntați cu erori ale bazei de date, cum ar fi baza de date neinițializată sau interogări care nu returnează rânduri.

Extinderea API-ului dvs

Cadrul acceptă dezvoltarea de pluginuri pentru a vă extinde API-ul și pentru a adăuga mai multe funcții cu ușurință. Goa are un depozit pentru pluginuri create de comunitate.

După cum am explicat mai devreme, ca parte a ciclului de viață al dezvoltării, ne putem baza pe setul de instrumente pentru a extinde aplicația noastră, revenind la definiția designului, actualizându-l și reîmprospătând codul nostru generat. Să arătăm cum pot ajuta pluginurile adăugând CORS și autentificare la API.

Actualizați fișierul clients/design/design.go la conținutul de mai jos:

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

Puteți observa două diferențe majore în noul design. Am definit un domeniu de securitate în serviciul client , astfel încât să putem valida dacă un utilizator este autorizat să invoce serviciul și am definit un al doilea serviciu numit signin , pe care îl vom folosi pentru a autentifica utilizatorii și a genera JSON Web Tokens (JWT), pe care serviciul client va folosi pentru a autoriza apelurile. De asemenea, am adăugat mai multe câmpuri la Tipul nostru de client personalizat. Acesta este un caz obișnuit atunci când se dezvoltă un API - nevoia de a remodela sau de a restructura datele.

În ceea ce privește designul, aceste modificări pot suna simple, dar reflectând la ele, există o mulțime de caracteristici minime necesare pentru a realiza ceea ce este descris în design. Luați, de exemplu, schemele arhitecturale pentru autentificare și autorizare folosind metodele noastre API:

Dezvoltare API în Go: scheme arhitecturale pentru autentificare și autorizare

Toate acestea sunt funcții noi pe care codul nostru nu le are încă. Din nou, aici Goa adaugă mai multă valoare eforturilor dumneavoastră de dezvoltare. Să implementăm aceste caracteristici în partea de transport, regenerând din nou codul sursă cu comanda de mai jos:

 goa gen clients/design

În acest moment, dacă se întâmplă să utilizați Git, veți observa prezența unor fișiere noi, altele fiind afișate ca actualizate. Acest lucru se datorează faptului că Goa a reîmprospătat perfect codul standard în consecință, fără intervenția noastră.

Acum, trebuie să implementăm codul de service. Într-o aplicație din lumea reală, ați actualiza manual aplicația după actualizarea sursei pentru a reflecta toate modificările de design. Acesta este modul în care Goa recomandă să procedăm, dar pentru concizie, voi șterge și regenera aplicația exemplu pentru a ajunge mai repede acolo. Rulați comenzile de mai jos pentru a șterge aplicația exemplu și pentru a o regenera:

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

Cu asta, codul dvs. ar trebui să arate astfel:

Dezvoltare API în Go: regenerare

Putem vedea un fișier nou în aplicația noastră exemplu: signin.go , care conține logica serviciului de conectare. Cu toate acestea, putem vedea că client.go a fost actualizat și cu o funcție JWTAuth pentru validarea token-urilor. Acest lucru se potrivește cu ceea ce am scris în design, astfel încât fiecare apel către orice metodă din client va fi interceptat pentru validarea simbolului și redirecționat numai dacă este autorizat de un simbol valid și un domeniu de aplicare corect.

Prin urmare, vom actualiza metodele din Serviciul nostru de conectare din interiorul signin.go pentru a adăuga logica pentru a genera token-urile pe care API-ul le va crea pentru utilizatorii autentificați. Copiați și inserați următorul context în 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 }

În cele din urmă, deoarece am adăugat mai multe câmpuri la tipul nostru personalizat, trebuie să actualizăm metoda Add pe client Service în client.go pentru a reflecta astfel de modificări. Copiați și inserați următoarele pentru a vă actualiza 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 }

Si asta e! Să recompilăm aplicația și să o testăm din nou. Rulați comenzile de mai jos pentru a elimina vechile binare și a compila altele noi:

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

Rulați din nou ./clients și lăsați-l să ruleze. Ar trebui să vedeți că rulează cu succes, dar de data aceasta, cu noile metode implementate:

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

Pentru a testa, haideți să executăm toate metodele API folosind cli - observați că folosim acreditările codificate:

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

Și iată-ne! Avem o aplicație minimalistă cu autentificare adecvată, autorizare a domeniului de aplicare și spațiu pentru creștere evolutivă. După aceasta, puteți dezvolta propria strategie de autentificare folosind servicii cloud sau orice alt furnizor de identitate la alegere. De asemenea, puteți crea pluginuri pentru baza de date sau sistemul de mesagerie preferat sau chiar să vă integrați cu ușurință cu alte API-uri.

Consultați proiectul GitHub de la Goa pentru mai multe plugin-uri, exemple (care arată capabilitățile specifice ale cadrului) și alte resurse utile.

Asta e pentru azi. Sper că ți-a plăcut să te joci cu Goa și să citești acest articol. Dacă aveți feedback despre conținut, nu ezitați să contactați GitHub, Twitter sau LinkedIn.

De asemenea, ieșim pe canalul #goa pe Gophers Slack, așa că vino și salută-te!

Pentru mai multe informații despre Golang, consultați Limbajul de programare Go: un tutorial introductiv Golang și Logic bine structurat: un tutorial Golang OOP.