API-kehitys on nykyään kuuma aihe. API: n kehittämiseen ja toimittamiseen on valtava määrä tapoja, ja suuret yritykset ovat kehittäneet massiivisia ratkaisuja, jotka auttavat sinua käynnistämään sovelluksen nopeasti.
Suurimmalla osalla näistä vaihtoehdoista ei kuitenkaan ole pääominaisuutta: kehityksen elinkaaren hallintaa. Joten kehittäjät käyttävät joitain syklejä luomalla hyödyllisiä ja vankkoja sovellusliittymiä, mutta joutuvat kamppailemaan koodinsa odotetun orgaanisen kehityksen ja seurausten kanssa, joita pienellä muutoksella sovellusliittymässä on lähdekoodissa.
Vuonna 2016 Raphael Simon luotu Goa , kehys API-kehitykselle Golangissa, jonka elinkaari asettaa API-suunnittelun etusijalle. Goassa API-määritelmääsi ei kuvata vain koodina, vaan se on myös lähde, josta palvelinkoodi, asiakaskoodi ja dokumentaatio on johdettu. Tämä tarkoittaa, että koodisi on kuvattu API-määritelmässäsi käyttämällä DSol-kieliä (Golang Domain Specific Language), joka on sitten luotu goa clin avulla ja joka on toteutettu erillään sovelluksen lähdekoodista.
Siksi Goa loistaa. Se on ratkaisu, jolla on hyvin määritelty kehityksen elinkaaren sopimus ja joka perustuu hyviin käytäntöihin koodia luodessa (kuten eri toimialueiden ja huolenaiheiden jakaminen kerroksiksi, joten kuljetusnäkökohdat eivät häiritse sovelluksen liiketoiminnallisia näkökohtia) noudattaen puhdasta arkkitehtuurimallia, jos se on mahdollista moduulit luodaan sovelluksen kuljetus-, päätepiste- ja liiketoimintalogiikkatasoille.
Jotkut Goan ominaisuudet, sellaisina kuin ne on määritelty virallinen nettisivu , sisältää:
Tässä artikkelissa luon sovelluksen ja opastan sinut API-kehityksen elinkaaren vaiheisiin. Sovellus hallinnoi asiakkaita koskevia yksityiskohtia, kuten nimeä, osoitetta, puhelinnumeroa, sosiaalista mediaa jne. Loppujen lopuksi yritämme laajentaa sitä ja lisätä uusia ominaisuuksia kehityksen elinkaaren käyttämiseksi.
Joten, aloitetaan!
Ensimmäinen vaihe on aloittaa arkisto ja ottaa Go-moduulituki käyttöön:
mkdir -p clients/design cd clients go mod init clients
Loppujen lopuksi repo-rakenteesi tulisi olla kuten alla:
$ tree . ├── design └── go.mod
API: n totuuden lähde on suunnittelumääritelmäsi. Kuten dokumentaatiossa todetaan, 'Goa antaa sinun ajatella sovellusliittymiäsi riippumatta toteutuksen huolenaiheista ja tarkistaa sitten suunnittelu kaikkien sidosryhmien kanssa ennen toteutuksen kirjoittamista.' Tämä tarkoittaa, että kaikki API-elementit määritetään tässä ensin, ennen kuin varsinainen sovelluskoodi luodaan. Mutta tarpeeksi puhumista!
Avaa tiedosto clients/design/design.go
ja lisää sisältö alla:
/* 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') })
Ensimmäinen asia, jonka voit huomata, on, että yllä oleva DSL on joukko Go-toimintoja, jotka voidaan muodostaa kuvaamaan etäpalvelun sovellusliittymää. Funktiot koostuvat käyttämällä nimettömiä funktion argumentteja. DSL-funktioissa meillä on joukko toimintoja, joiden ei pitäisi näkyä muiden toimintojen sisällä, joita kutsumme ylätason DSL :eiksi. Alla on osittainen joukko DSL-toimintoja ja niiden rakennetta:
Joten meillä on alkuperäisessä suunnittelussa API: n ylätason DSL, joka kuvaa asiakkaan API: ta, yksi palvelun ylätason DSL: t, jotka kuvaavat pääasiallista API-palvelua, clients
ja palvelevat API: n swagger-tiedostoa, ja kaksi tyyppiä: ylätason DSL: t kuljetuksen hyötykuormassa käytetyn objektinäkymätyypin kuvaamiseen.
API
function on valinnainen ylätason DSL, joka listaa API: n globaalit ominaisuudet, kuten nimen, kuvauksen ja myös yhden tai useamman palvelimen, jotka saattavat paljastaa erilaisia palvelusarjoja. Meidän tapauksessamme yksi palvelin riittää, mutta voit palvella myös erilaisia palveluja eri tasoilla: esimerkiksi kehitys, testaus ja tuotanto.
Service
funktio määrittelee ryhmän menetelmiä, jotka mahdollisesti kartoitetaan kuljetuksen resurssiin. Palvelu voi myös määritellä yleisiä virhereaktioita. Palvelumenetelmät kuvataan käyttämällä Method
. Tämä toiminto määrittelee menetelmän hyötykuorma (syöttö) ja tulos (lähtö) tyypit. Jos jätät hyötykuorman tai tuloslajin pois, käytetään sisäänrakennettua tyyppiä Tyhjä, joka kartoittaa tyhjään runkoon HTTP: ssä.
Lopuksi Type
tai ResultType
funktiot määrittelevät käyttäjän määrittelemät tyypit, tärkein ero on, että tulostyyppi määrittelee myös joukon 'näkymiä'.
Esimerkissämme kuvasimme sovellusliittymän ja selitimme, kuinka sen pitäisi toimia, ja loimme myös seuraavat:
clients
add
(yhden asiakkaan luomiseen), get
(yhden asiakkaan hakemiseksi) ja show
(kaikkien asiakkaiden luetteloimiseksi)Nyt kun sovelluksemme on kuvattu, voimme luoda kattilakoodin. Seuraava komento ottaa suunnittelupaketin tuontipolun argumentiksi. Se hyväksyy myös polun lähtöhakemistoon valinnaisena lipuna:
goa gen clients/design
Komento antaa sen luomien tiedostojen nimet. Siellä gen
hakemisto sisältää sovelluksen nimen alihakemiston, joka sisältää kuljetuksesta riippumattoman palvelukoodin. http
alihakemisto kuvaa HTTP-kuljetuksen (meillä on palvelin- ja asiakaskoodi sekä logiikka pyyntöjen ja vastausten koodaamiseksi ja dekoodaamiseksi ja CLI-koodi HTTP-pyyntöjen muodostamiseksi komentoriviltä). Se sisältää myös Open API 2.0 -määritystiedostot sekä JSON- että YAML-muodossa.
Voit kopioida swagger-tiedoston sisällön ja liittää sen mihin tahansa online-Swagger-editoriin (kuten osoitteessa swagger.io ) API-määrittelydokumentaation visualisoimiseksi. Ne tukevat sekä YAML- että JSON-muotoja.
Olemme nyt valmiita seuraavaan vaiheeseen kehityksen elinkaaressa.
Kun kattilakoodisi on luotu, on aika lisätä siihen liiketoimintalogiikkaa. Tässä vaiheessa koodisi tulisi näyttää seuraavalta:
Goa ylläpitää ja päivittää jokaista yllä olevaa tiedostoa aina, kun suoritamme CLI: n. Siten, kun arkkitehtuuri kehittyy, suunnittelusi seuraa kehitystä ja niin myös lähdekoodisi. Sovelluksen toteuttamiseksi suoritamme alla olevan komennon (se luo palvelun perustoteutuksen yhdessä rakennettavien palvelintiedostojen kanssa, jotka pyörittävät gorutiineja aloittaakseen HTTP-palvelimen ja asiakastiedostot, jotka voivat tehdä pyyntöjä tälle palvelimelle):
goa example clients/design
Tämä luo cmd-kansion, jossa on sekä palvelimen että asiakkaan rakennettavat lähteet. Siellä on sovelluksesi, ja nämä ovat tiedostoja, joita sinun tulisi ylläpitää itsesi sen jälkeen, kun Goa on ensin luonut ne.
Goan dokumentaatio tekee selväksi, että: 'Tämä komento luo palvelulle lähtökohdan, joka auttaa käynnistyshihnan kehitystä - etenkään sitä ei ole tarkoitus suorittaa uudelleen, kun muotoilu muuttuu.'
Nyt koodisi näyttää tältä:
Missä client.go
on esimerkkitiedosto, jossa molemmat get
ja show
menetelmiä. Lisätään siihen liiketoimintalogiikkaa!
Yksinkertaisuuden vuoksi käytämme SQLitea muistin sisäisen tietokannan sijaan ja Gormia ORM: na. Luo tiedosto sqlite.go
ja lisää alla oleva sisältö - joka lisää tietokantalogiikan luomaan tietueita tietokantaan ja luetteloiden yhden ja / tai useita rivejä tietokannasta:
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 }
Sitten muokkaamme client.go: ta päivittämään kaikki asiakaspalvelun menetelmät, toteuttamalla tietokantakutsut ja rakentamalla API-vastaukset:
// 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 }
Sovelluksemme ensimmäinen osa on valmis koottavaksi. Suorita seuraava komento luodaksesi palvelimen ja asiakkaan binäärit:
go build ./cmd/clients go build ./cmd/clients-cli
Suorita palvelin suorittamalla vain ./clients
. Jätä se toistaiseksi. Sinun pitäisi nähdä se toimivan onnistuneesti, kuten seuraavat:
$ ./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'
Olemme valmiita suorittamaan joitain testejä sovelluksessamme. Kokeile kaikkia menetelmiä käyttämällä 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' } ]
Jos saat virheilmoituksen, tarkista palvelinlokit varmistaaksesi, että SQLite ORM -logiikka on hyvä etkä kohdata mitään tietokantavirheitä, kuten alustamatonta tietokantaa tai kyselyitä, jotka eivät palauta rivejä.
Kehys tukee laajennusten kehittämistä sovellusliittymän laajentamiseksi ja uusien ominaisuuksien lisäämiseksi helposti. Goalla on arkisto yhteisön luomille laajennuksille.
Kuten aiemmin selitin, osana kehityksen elinkaarta voimme luottaa työkalupakettiin sovelluksen laajentamiseksi palaamalla suunnittelumääritelmään, päivittämällä sen ja päivittämällä luodun koodin. Esitellään, kuinka laajennukset voivat auttaa lisäämällä CORS: n ja todennuksen sovellusliittymään.
Päivitä tiedosto clients/design/design.go
alla olevaan sisältöön:
/* This is the design file. It contains the API specification, methods, inputs, and outputs using Goa DSL code. The objective is to use this as a single source of truth for the entire API source code. */ package design import ( . 'goa.design/goa/v3/dsl' 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) }) }) })
Voit huomata kaksi suurta eroa uudessa suunnittelussa. Määritimme suojausalueen client
palvelua, jotta voimme vahvistaa, jos käyttäjällä on lupa käyttää palvelua, ja määritimme toisen palvelun nimeltä signin
, jota käytämme käyttäjien todentamiseen ja JSON-verkkotunnusten (JWT) luomiseen, joita client
Palvelu käyttää puhelujen valtuuttamiseen. Olemme myös lisänneet lisää kenttiä mukautettuun asiakastyyppiin. Tämä on yleinen tapaus, kun kehitetään sovellusliittymää - tarve muotoilla tai järjestellä tietoja uudelleen.
Suunnittelussa nämä muutokset saattavat kuulostaa yksinkertaisilta, mutta heijastellen niitä, suunnittelussa kuvatun saavuttamiseksi tarvitaan paljon vähäisiä ominaisuuksia. Otetaan esimerkiksi arkkitehtuurikaaviot todennusta ja todennusta varten API-menetelmillä:
Nämä ovat kaikki uusia ominaisuuksia, joita koodissamme ei vielä ole. Jälleen tässä Goa lisää enemmän arvoa kehitystyöhön. Toteutetaan nämä ominaisuudet siirtopuolella regeneroimalla lähdekoodi uudelleen alla olevalla komennolla:
goa gen clients/design
Tässä vaiheessa, jos satut käyttämään Git-ohjelmaa, huomaat uusien tiedostojen esiintymisen, kun taas muut näkyvät päivitetyinä. Tämä johtuu siitä, että Goa päivitti saumattomasti kattilakoodin vastaavasti ilman meidän puuttumistamme.
Nyt meidän on otettava käyttöön palvelun sivukoodi. Todellisessa sovelluksessa päivität sovelluksen manuaalisesti lähteen päivittämisen jälkeen vastaamaan kaikkia suunnittelumuutoksia. Goa suosittelee tällä tavoin, että jatkamme, mutta lyhyyden vuoksi poistan ja uudistan esimerkkisovelluksen, jotta pääsen sinne nopeammin. Suorita alla olevat komennot poistaaksesi esimerkkisovelluksen ja luodaksesi sen uudelleen:
rm -rf cmd client.go goa example clients/design
Tämän avulla koodisi pitäisi näyttää seuraavalta:
Voimme nähdä yhden uuden tiedoston esimerkkisovelluksessamme: signin.go
, joka sisältää kirjautumispalvelulogiikan. Voimme kuitenkin nähdä, että client.go
päivitettiin myös JWTAuth-toiminnolla rahakkeiden vahvistamiseksi. Tämä vastaa mitä olemme kirjoittaneet suunnitteluun, joten jokainen asiakkaan kutsu mihin tahansa menetelmään siepataan tunnuksen vahvistamista varten ja edelleenlähetetään vain, jos se on sallittu kelvollisella tunnuksella ja oikealla laajuudella.
Siksi päivitämme kirjautumispalvelumme menetelmät sisällä signin.go
jotta logiikka voidaan lisätä tunnusten luomiseen, sovellusliittymä luo todennetuille käyttäjille. Kopioi ja liitä seuraava konteksti kansioon 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 }
Lopuksi, koska lisäsimme lisää kenttiä mukautettuun tyyppiin, meidän on päivitettävä asiakaspalvelun Lisää-menetelmä client.go
vastaamaan tällaisia muutoksia. Kopioi ja liitä seuraava päivittääksesi 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 }
Ja siinä kaikki! Kootaan sovellus uudelleen ja testataan se uudelleen. Suorita alla olevat komennot poistaa vanhat binäärit ja koota uudet:
rm -f clients clients-cli go build ./cmd/clients go build ./cmd/clients-cli
Suorita ./clients
uudelleen ja jätä se käyntiin. Sinun pitäisi nähdä se toimivan onnistuneesti, mutta tällä kertaa uusien menetelmien kanssa:
$ ./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'
Testataksemme, suoritetaan kaikki sovellusliittymämenetelmät clin avulla - huomaa, että käytämme kovakoodattuja tunnistetietoja:
$ ./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 } ]
Ja siellä me mennään! Meillä on minimalistinen sovellus, jolla on asianmukainen todennus, laajuusvaltuutus ja tilaa evoluutiokasvulle. Tämän jälkeen voit kehittää oman todennusstrategian pilvipalveluiden tai minkä tahansa muun valitsemasi identiteettipalvelun avulla. Voit myös luoda laajennuksia haluamallesi tietokannalle tai viestijärjestelmälle tai jopa integroida helposti muihin sovellusliittymiin.
Katso Goaa GitHub-projekti lisää laajennuksia, esimerkkejä (jotka osoittavat kehyksen erityisominaisuudet) ja muita hyödyllisiä resursseja.
Se on tänään. Toivottavasti olet nauttinut pelaamisesta Goan kanssa ja tämän artikkelin lukemisesta. Jos sinulla on palautetta sisällöstä, ota rohkeasti yhteyttä GitHub , Viserrys tai LinkedIn .
Olemme myös hengailla #goa-kanavalla Gophers löysä , niin tule eteenpäin ja sano hei!
Lisätietoja Golangista, katso Mene ohjelmointikieli: Johdanto Golang-opetusohjelma ja Hyvin jäsennelty logiikka: Golang OOP -opetusohjelma .
Yleensä API on samanlainen kuin menetelmäpuhelu, paitsi että sen kutsuparametrit ja palautukset julkaistaan laajaan käyttöön. Se palauttaa yleensä laskennan tai tiedot, jotka vastaavat sen ilmoitettua tarkoitusta. API voi myös käyttää REST (edustustilan siirto) ja on samanlainen kuin Web-URL-osoitteen kutsuminen.
Sovellusliittymistä on tullut yhä tärkeämpi tapa asettaa tietyt sovellusominaisuudet yrityksen tai asiakkaiden saataville. Esimerkiksi kolmannen osapuolen Amazon-myyjät käyttävät yrityksen sovellusliittymiä pääsyn alustalle, mikä antaa heille mahdollisuuden olla paljon laajempi tavoite kuin muutoin.
Golangia käytetään usein nopeana tapana prototyyppien kirjoittamiseen tai komentosarjojen kirjoittamiseen. Kieli on poikkeuksellisen helppo oppia, ja kehittäjät voivat käyttää sitä välipalana muiden kielten oppimiselle.
Vaikka Golangilla ei välttämättä ole yleistä kieltä, sillä on pienempi mutta äänekäs ja aktiivinen käyttäjäyhteisö, mikä takaa kielelle merkittävän roolin tulevina vuosina.