Rozwój API w Go przy użyciu Goa

Opublikowany: 2022-03-11

Rozwój API to obecnie gorący temat. Istnieje wiele sposobów tworzenia i dostarczania interfejsu API, a duże firmy opracowały ogromne rozwiązania ułatwiające szybkie uruchamianie aplikacji.

Jednak większości z tych opcji brakuje kluczowej funkcji: zarządzania cyklem życia rozwoju. Tak więc programiści spędzają kilka cykli na tworzeniu użytecznych i niezawodnych interfejsów API, ale w końcu zmagają się z oczekiwaną organiczną ewolucją swojego kodu i implikacjami, jakie niewielka zmiana w interfejsie API ma w kodzie źródłowym.

W 2016 roku Raphael Simon stworzył Goa, framework do tworzenia API w Golang z cyklem życia, który stawia projektowanie API na pierwszym miejscu. W Goa definicja interfejsu API jest nie tylko opisana jako kod, ale jest także źródłem, z którego pochodzi kod serwera, kod klienta i dokumentacja. Oznacza to, że Twój kod jest opisany w definicji interfejsu API przy użyciu języka Golang Domain Specific Language (DSL), a następnie generowany przy użyciu goa CLI i implementowany oddzielnie od kodu źródłowego aplikacji.

To jest powód, dla którego Goa świeci. Jest to rozwiązanie z dobrze zdefiniowaną umową cyklu życia deweloperskiego, która opiera się na najlepszych praktykach podczas generowania kodu (takich jak dzielenie różnych domen i problemów na warstwy, aby aspekty transportowe nie kolidowały z aspektami biznesowymi aplikacji), zgodnie z wzorcem czystej architektury, jeśli jest to możliwe do komponowania moduły są generowane dla warstwy transportu, punktu końcowego i logiki biznesowej w aplikacji.

Niektóre funkcje Goa, zgodnie z definicją na oficjalnej stronie internetowej, obejmują:

  • Kompozycyjność . Pakiet, algorytmy generowania kodu i wygenerowany kod są modułowe.
  • Niezależny od transportu . Oddzielenie warstwy transportowej od rzeczywistej implementacji usługi oznacza, że ​​ta sama usługa może uwidaczniać punkty końcowe dostępne za pośrednictwem wielu transportów, takich jak HTTP i/lub gRPC.
  • Separacja obaw . Rzeczywista implementacja usługi jest odizolowana od kodu transportowego.
  • Wykorzystanie standardowych typów bibliotek Go . Ułatwia to interfejs z zewnętrznym kodem.

W tym artykule stworzę aplikację i przeprowadzę Cię przez etapy cyklu rozwoju API. Aplikacja zarządza szczegółami o klientach, takimi jak imię i nazwisko, adres, numer telefonu, media społecznościowe itp. Na koniec postaramy się ją rozszerzyć i dodać nowe funkcje, aby przećwiczyć jej cykl rozwoju.

Więc zacznijmy!

Przygotowanie obszaru rozwoju

Naszym pierwszym krokiem jest uruchomienie repozytorium i włączenie obsługi modułów Go:

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

Ostatecznie struktura twojego repozytorium powinna wyglądać jak poniżej:

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

Projektowanie Twojego API

Źródłem prawdy dla twojego API jest twoja definicja projektu. Jak stwierdza dokumentacja, „Goa pozwala myśleć o interfejsach API niezależnie od jakichkolwiek problemów związanych z implementacją, a następnie przejrzeć ten projekt ze wszystkimi zainteresowanymi stronami przed napisaniem implementacji”. Oznacza to, że każdy element API jest tutaj definiowany jako pierwszy, zanim zostanie wygenerowany rzeczywisty kod aplikacji. Ale dość gadania!

Otwórz plik clients/design/design.go i dodaj zawartość poniżej:

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

Pierwszą rzeczą, jaką można zauważyć, jest to, że powyższe DSL to zestaw funkcji Go, które można skomponować w celu opisania interfejsu API usług zdalnych. Funkcje składają się z anonimowych argumentów funkcji. W funkcjach DSL mamy podzbiór funkcji, które nie powinny pojawiać się w innych funkcjach, które nazywamy DSL najwyższego poziomu. Poniżej masz częściowy zestaw funkcji DSL i ich strukturę:

Funkcje DSL

Tak więc w naszym początkowym projekcie mamy DSL najwyższego poziomu API opisujący API naszego klienta, jedną DSL najwyższego poziomu usługi opisującą główną usługę API, clients i obsługującą plik swagger API oraz dwa typy DSL najwyższego poziomu opisujące typ widoku obiektu używany w ładunku transportowym.

Funkcja API to opcjonalna usługa DSL najwyższego poziomu, która wyświetla globalne właściwości interfejsu API, takie jak nazwa, opis, a także jeden lub więcej serwerów potencjalnie narażających różne zestawy usług. W naszym przypadku wystarczy jeden serwer, ale możesz też obsługiwać różne usługi na różnych poziomach: na przykład deweloperski, testowy i produkcyjny.

Funkcja Service definiuje grupę metod, które potencjalnie mapują zasób w transporcie. Usługa może również definiować typowe reakcje na błędy. Sposoby obsługi są opisane za pomocą Method . Ta funkcja definiuje typy ładunku (wejście) i wynik (wyjście) metody. W przypadku pominięcia typu ładunku lub wyniku zostanie użyty wbudowany typ Empty, który jest mapowany na pustą treść w HTTP.

Wreszcie funkcje Type lub ResultType definiują typy zdefiniowane przez użytkownika, przy czym główna różnica polega na tym, że typ wyniku definiuje również zestaw „widoków”.

W naszym przykładzie opisaliśmy API i wyjaśniliśmy, jak ma służyć, a także stworzyliśmy:

  • Usługa o nazwie clients
  • Trzy metody: add (w celu utworzenia jednego klienta), get (w celu pobrania jednego klienta) i show (w celu wyświetlenia wszystkich klientów)
  • Własne typy niestandardowe, które przydadzą się podczas integracji z bazą danych oraz niestandardowy typ błędu

Teraz, gdy nasza aplikacja została opisana, możemy wygenerować kod wzorcowy. Następujące polecenie przyjmuje ścieżkę importu pakietu projektu jako argument. Akceptuje również ścieżkę do katalogu wyjściowego jako opcjonalną flagę:

 goa gen clients/design

Polecenie wyświetla nazwy generowanych plików. Tam katalog gen zawiera podkatalog nazwy aplikacji, w którym znajduje się kod usługi niezależny od transportu. Podkatalog http opisuje transport HTTP (mamy kod serwera i klienta z logiką do kodowania i dekodowania żądań i odpowiedzi oraz kod CLI do budowania żądań HTTP z wiersza poleceń). Zawiera również pliki specyfikacji Open API 2.0 w formatach JSON i YAML.

Możesz skopiować zawartość pliku swagger i wkleić go do dowolnego internetowego edytora Swagger (takiego jak ten na swagger.io) w celu wizualizacji dokumentacji specyfikacji API. Obsługują zarówno formaty YAML, jak i JSON.

Jesteśmy teraz gotowi na kolejny krok w cyklu rozwoju.

Implementacja Twojego API

Po utworzeniu kodu wzorcowego nadszedł czas, aby dodać do niego logikę biznesową. W tym momencie twój kod powinien wyglądać tak:

Rozwój API w Go

Gdzie każdy powyższy plik jest utrzymywany i aktualizowany przez Goa za każdym razem, gdy uruchamiamy CLI. W związku z tym, wraz z rozwojem architektury, twój projekt będzie podążał za ewolucją, podobnie jak twój kod źródłowy. Aby zaimplementować aplikację, wykonujemy poniższe polecenie (wygeneruje podstawową implementację usługi wraz z plikami serwera do zbudowania, które uruchamiają gorutyny do uruchomienia serwera HTTP oraz plikami klienta, które mogą wysyłać żądania do tego serwera):

 goa example clients/design

Spowoduje to wygenerowanie folderu cmd ze źródłami, które można zbudować zarówno na serwerze, jak i na kliencie. Będzie twoja aplikacja i to są pliki, które powinieneś sam utrzymywać po ich pierwszym wygenerowaniu przez Goa.

Dokumentacja Goa wyjaśnia, że: „To polecenie generuje punkt wyjścia dla usługi, aby pomóc w rozwoju ładowania początkowego – w szczególności NIE ma być ponownie uruchamiane, gdy zmieni się projekt”.

Teraz Twój kod będzie wyglądał tak:

Rozwój API w Go: folder cmd

Gdzie client.go to przykładowy plik z fikcyjną implementacją metod get i show . Dodajmy do tego trochę logiki biznesowej!

Dla uproszczenia użyjemy SQLite zamiast bazy danych w pamięci, a Gorm jako naszego ORM-a. Utwórz plik sqlite.go i dodaj zawartość poniżej - to doda logikę bazy danych do tworzenia rekordów w bazie danych i listy jednego i/lub wielu wierszy z bazy danych:

 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 }

Następnie edytujemy client.go, aby zaktualizować wszystkie metody w Client Service, implementując wywołania bazy danych i konstruując odpowiedzi 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 }

Pierwszy krój naszej aplikacji jest gotowy do skompilowania. Uruchom następujące polecenie, aby utworzyć pliki binarne serwera i klienta:

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

Aby uruchomić serwer, wystarczy uruchomić ./clients . Zostaw to na razie włączone. Powinieneś zobaczyć, jak działa pomyślnie, jak poniżej:

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

Jesteśmy gotowi do przeprowadzenia testów w naszej aplikacji. Wypróbujmy wszystkie metody za pomocą 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" } ]

Jeśli pojawi się jakiś błąd, sprawdź dzienniki serwera, aby upewnić się, że logika SQLite ORM jest dobra i nie napotykasz żadnych błędów bazy danych, takich jak baza danych nie została zainicjowana lub zapytania nie zwracają żadnych wierszy.

Rozszerzenie Twojego API

Framework wspiera rozwój wtyczek, aby rozszerzyć twoje API i łatwo dodać więcej funkcji. Goa posiada repozytorium wtyczek stworzonych przez społeczność.

Jak wyjaśniłem wcześniej, w ramach cyklu rozwoju, możemy polegać na zestawie narzędzi, aby rozszerzyć naszą aplikację, wracając do definicji projektu, aktualizując ją i odświeżając wygenerowany kod. Pokażmy, jak wtyczki mogą pomóc, dodając CORS i uwierzytelnianie do interfejsu API.

Zaktualizuj plik clients/design/design.go do treści poniżej:

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

W nowym projekcie można zauważyć dwie zasadnicze różnice. Zdefiniowaliśmy zakres bezpieczeństwa w usłudze client , abyśmy mogli sprawdzić, czy użytkownik jest upoważniony do wywoływania usługi, a także zdefiniowaliśmy drugą usługę o nazwie signin , której będziemy używać do uwierzytelniania użytkowników i generowania tokenów JSON Web Token (JWT), które obsługa client użyje do autoryzacji połączeń. Dodaliśmy również więcej pól do naszego niestandardowego typu klienta. Jest to częsty przypadek podczas tworzenia interfejsu API — potrzeba zmiany kształtu lub restrukturyzacji danych.

Jeśli chodzi o projekt, te zmiany mogą wydawać się proste, ale biorąc pod uwagę je, istnieje wiele minimalnych funkcji wymaganych do osiągnięcia tego, co opisano w projekcie. Weźmy na przykład schematy architektoniczne uwierzytelniania i autoryzacji przy użyciu naszych metod API:

API Development in Go: schematy architektoniczne do uwierzytelniania i autoryzacji

To wszystko są nowe funkcje, których nasz kod jeszcze nie ma. Ponownie, w tym miejscu Goa dodaje więcej wartości do twoich wysiłków rozwojowych. Zaimplementujmy te funkcje po stronie transportu, ponownie generując kod źródłowy za pomocą poniższego polecenia:

 goa gen clients/design

W tym momencie, jeśli używasz Git, zauważysz obecność nowych plików, a inne będą wyświetlane jako zaktualizowane. Dzieje się tak dlatego, że Goa bez naszej ingerencji bezproblemowo odświeżył odpowiednio standardowy kod.

Teraz musimy zaimplementować kod po stronie usługi. W rzeczywistej aplikacji ręcznie aktualizujesz aplikację po zaktualizowaniu źródła, aby odzwierciedlić wszystkie zmiany w projekcie. To jest sposób, w jaki Goa zaleca, abyśmy postępowali, ale dla zwięzłości usunę i zregeneruję przykładową aplikację, aby szybciej się tam dostać. Uruchom poniższe polecenia, aby usunąć przykładową aplikację i zregenerować ją:

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

Dzięki temu Twój kod powinien wyglądać tak:

Rozwój API w Go: regeneracja

W naszej przykładowej aplikacji widzimy jeden nowy plik: signin.go , który zawiera logikę usługi logowania. Widzimy jednak, że client.go został również zaktualizowany o funkcję JWTAuth do walidacji tokenów. Jest to zgodne z tym, co napisaliśmy w projekcie, więc każde wywołanie dowolnej metody w kliencie zostanie przechwycone w celu sprawdzenia poprawności tokenu i przekazane dalej tylko wtedy, gdy zostanie autoryzowane przez poprawny token i poprawny zakres.

Dlatego zaktualizujemy metody w naszej usłudze logowania wewnątrz signin.go , aby dodać logikę generowania tokenów, które API utworzy dla uwierzytelnionych użytkowników. Skopiuj i wklej następujący kontekst do 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 }

Wreszcie, ponieważ dodaliśmy więcej pól do naszego niestandardowego typu, musimy zaktualizować metodę Add w usłudze klienta w client.go , aby odzwierciedlić takie zmiany. Skopiuj i wklej następujące informacje, aby zaktualizować 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 }

I to wszystko! Przekompilujmy aplikację i przetestujmy ją jeszcze raz. Uruchom poniższe polecenia, aby usunąć stare pliki binarne i skompilować nowe:

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

Uruchom ponownie ./clients i pozostaw uruchomiony. Powinieneś zobaczyć, jak działa pomyślnie, ale tym razem z zaimplementowanymi nowymi metodami:

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

Aby przetestować, wykonajmy wszystkie metody API za pomocą cli — zauważ, że używamy danych uwierzytelniających zakodowanych na sztywno:

 $ ./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 gotowe! Mamy minimalistyczną aplikację z odpowiednim uwierzytelnianiem, autoryzacją zakresu i miejscem na ewolucyjny rozwój. Następnie możesz opracować własną strategię uwierzytelniania za pomocą usług w chmurze lub dowolnego innego dostawcy tożsamości. Możesz także tworzyć wtyczki do preferowanej bazy danych lub systemu przesyłania wiadomości, a nawet łatwo integrować się z innymi interfejsami API.

Sprawdź projekt GitHub Goa, aby uzyskać więcej wtyczek, przykładów (pokazujących konkretne możliwości frameworka) i innych przydatnych zasobów.

To tyle na dzisiaj. Mam nadzieję, że podobało Ci się granie z Goa i czytanie tego artykułu. Jeśli masz jakieś uwagi na temat treści, skontaktuj się z GitHub, Twitterem lub LinkedIn.

Poza tym spędzamy czas na kanale #goa na Gophers Slack, więc wpadnij i przywitaj się!

Aby uzyskać więcej informacji na temat Golanga, zobacz Język programowania Go: wprowadzający samouczek Golanga i Dobrze ustrukturyzowana logika: samouczek Golanga OOP.