Goa Kullanarak Go'da API Geliştirme

Yayınlanan: 2022-03-11

API geliştirme, günümüzde sıcak bir konudur. Bir API geliştirip sunmanın çok sayıda yolu vardır ve büyük şirketler, bir uygulamayı hızlı bir şekilde başlatmanıza yardımcı olacak devasa çözümler geliştirmiştir.

Yine de bu seçeneklerin çoğu önemli bir özellikten yoksundur: geliştirme yaşam döngüsü yönetimi. Bu nedenle, geliştiriciler yararlı ve sağlam API'ler oluşturmak için bazı döngüler harcarlar, ancak sonunda kodlarının beklenen organik gelişimi ve API'deki küçük bir değişikliğin kaynak kodundaki etkileri ile mücadele ederler.

2016'da Raphael Simon, Golang'da API tasarımına öncelik veren bir yaşam döngüsüne sahip bir API geliştirme çerçevesi olan Goa'yı yarattı. Goa'da API tanımınız yalnızca kod olarak tanımlanmaz, aynı zamanda sunucu kodunun, istemci kodunun ve belgelerin türetildiği kaynaktır. Bu, kodunuzun API tanımınızda bir Golang Etki Alanına Özgü Dil (DSL) kullanılarak tanımlandığı, ardından goa cli kullanılarak oluşturulduğu ve uygulama kaynak kodunuzdan ayrı olarak uygulandığı anlamına gelir.

Goa'nın parlamasının nedeni budur. Kod oluştururken en iyi uygulamalara dayanan iyi tanımlanmış bir geliştirme yaşam döngüsü sözleşmesine sahip bir çözümdür (farklı etki alanlarını ve endişeleri katmanlara bölmek gibi, böylece taşıma yönleri uygulamanın işle ilgili yönlerini etkilemez), oluşturulabilir olduğunda temiz bir mimari modeli izler. uygulamanızdaki taşıma, uç nokta ve iş mantığı katmanları için modüller oluşturulur.

Resmi web sitesinde tanımlandığı şekliyle bazı Goa özellikleri şunları içerir:

  • birleştirilebilirlik Paket, kod oluşturma algoritmaları ve oluşturulan kodun tümü modülerdir.
  • Taşımadan bağımsız . Aktarım katmanının gerçek hizmet uygulamasından ayrılması, aynı hizmetin HTTP ve/veya gRPC gibi birden çok aktarım yoluyla erişilebilen uç noktaları açığa çıkarabileceği anlamına gelir.
  • Endişelerin ayrılması . Gerçek hizmet uygulaması, taşıma kodundan yalıtılmıştır.
  • Go standart kitaplık türlerinin kullanımı . Bu, harici kodla arayüz oluşturmayı kolaylaştırır.

Bu yazıda, bir uygulama oluşturacağım ve API geliştirme yaşam döngüsünün aşamalarında size yol göstereceğim. Uygulama, müşterilerle ilgili ad, adres, telefon numarası, sosyal medya vb. gibi ayrıntıları yönetir. Sonunda, onu genişletmeye ve geliştirme yaşam döngüsünü uygulamak için yeni özellikler eklemeye çalışacağız.

Öyleyse başlayalım!

Gelişim Alanınızı Hazırlama

İlk adımımız, depoyu başlatmak ve Go modülleri desteğini etkinleştirmektir:

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

Sonunda, repo yapınız aşağıdaki gibi olmalıdır:

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

API'nizi Tasarlama

API'niz için gerçeğin kaynağı, tasarım tanımınızdır. Belgelerin belirttiği gibi, "Goa, API'leriniz hakkında herhangi bir uygulama endişesinden bağımsız olarak düşünmenize ve ardından uygulamayı yazmadan önce bu tasarımı tüm paydaşlarla birlikte incelemenize olanak tanır." Bu, gerçek uygulama kodu oluşturulmadan önce API'nin her öğesinin ilk olarak burada tanımlandığı anlamına gelir. Ama yeterince konuşma!

clients/design/design.go dosyasını açın ve aşağıdaki içeriği ekleyin:

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

Fark edebileceğiniz ilk şey, yukarıdaki DSL'nin bir uzak hizmet API'sini tanımlamak için oluşturulabilecek bir dizi Go işlevi olmasıdır. İşlevler, anonim işlev bağımsız değişkenleri kullanılarak oluşturulur. DSL işlevlerinde, üst düzey DSL'ler olarak adlandırdığımız, diğer işlevlerde görünmemesi gereken bir işlev alt kümesine sahibiz. Aşağıda, kısmi bir dizi DSL işlevine ve bunların yapısına sahipsiniz:

DSL işlevleri

Bu nedenle, ilk tasarımımızda, müşterimizin API'sini açıklayan bir API üst düzey DSL'si, ana API hizmetini, clients açıklayan ve API swagger dosyasını sunan bir hizmet üst düzey DSL'leri ve açıklamak için iki tip üst düzey DSL'ye sahibiz. taşıma yükünde kullanılan nesne görünümü türü.

API işlevi, API'nin ad, açıklama gibi genel özelliklerini ve ayrıca potansiyel olarak farklı hizmet kümelerini açığa çıkaran bir veya daha fazla sunucuyu listeleyen isteğe bağlı bir üst düzey DSL'dir. Bizim durumumuzda bir sunucu yeterlidir, ancak farklı katmanlarda farklı hizmetler de sunabilirsiniz: örneğin geliştirme, test ve üretim.

Service işlevi, aktarımdaki bir kaynağa potansiyel olarak eşlenen bir yöntem grubu tanımlar. Bir hizmet ayrıca yaygın hata yanıtlarını da tanımlayabilir. Servis yöntemleri Method kullanılarak açıklanmıştır. Bu işlev, yöntem yükü (giriş) ve sonuç (çıkış) türlerini tanımlar. Yükü veya sonuç türünü atlarsanız, HTTP'de boş bir gövdeyle eşleşen yerleşik Boş türü kullanılır.

Son olarak, Type veya ResultType işlevleri, kullanıcı tanımlı türleri tanımlar; temel fark, bir sonuç türünün aynı zamanda bir dizi "görünüm" tanımlamasıdır.

Örneğimizde API'yi tanımladık ve nasıl hizmet etmesi gerektiğini açıkladık ve ayrıca aşağıdakileri oluşturduk:

  • clients adı verilen bir hizmet
  • Üç yöntem: add (bir istemci oluşturmak için), get (bir istemciyi almak için) ve show (tüm istemcileri listelemek için)
  • Bir veritabanıyla entegre ettiğimizde kullanışlı olacak kendi özel türlerimiz ve özelleştirilmiş bir hata türü

Artık uygulamamız anlatıldığına göre, kazan plakası kodunu üretebiliriz. Aşağıdaki komut, tasarım paketi içe aktarma yolunu bağımsız değişken olarak alır. Ayrıca çıktı dizinine giden yolu isteğe bağlı bir bayrak olarak kabul eder:

 goa gen clients/design

Komut, oluşturduğu dosyaların adlarını verir. Orada, gen dizini, taşımadan bağımsız hizmet kodunu barındıran uygulama adı alt dizinini içerir. http alt dizini, HTTP aktarımını tanımlar (istekleri ve yanıtları kodlamak ve kodunu çözmek için mantık içeren sunucu ve istemci koduna ve komut satırından HTTP istekleri oluşturmak için CLI koduna sahibiz). Ayrıca hem JSON hem de YAML biçimlerinde Open API 2.0 belirtim dosyalarını içerir.

API belirtim belgelerinizi görselleştirmek için bir swagger dosyasının içeriğini kopyalayabilir ve herhangi bir çevrimiçi Swagger düzenleyicisine (swagger.io'daki gibi) yapıştırabilirsiniz. Hem YAML hem de JSON formatlarını desteklerler.

Artık geliştirme yaşam döngüsündeki bir sonraki adımımız için hazırız.

API'nizi Uygulama

Standart kodunuz oluşturulduktan sonra, ona biraz iş mantığı eklemenin zamanı geldi. Bu noktada, kodunuz şöyle görünmelidir:

Go'da API Geliştirme

CLI'yi her çalıştırdığımızda, yukarıdaki her dosyanın Goa tarafından korunduğu ve güncellendiği yer. Böylece mimari geliştikçe tasarımınız ve kaynak kodunuz da evrimi takip edecek. Uygulamayı uygulamak için aşağıdaki komutu uygularız (bir HTTP sunucusu başlatmak için goroutinleri çalıştıran oluşturulabilir sunucu dosyaları ve bu sunucuya istekte bulunabilen istemci dosyaları ile birlikte hizmetin temel bir uygulamasını oluşturur):

 goa example clients/design

Bu, hem sunucu hem de istemci tarafından oluşturulabilir kaynaklara sahip bir cmd klasörü oluşturacaktır. Başvurunuz olacak ve bunlar, Goa onları ilk oluşturduktan sonra kendiniz tutmanız gereken dosyalar.

Goa belgeleri şunu açıkça ortaya koymaktadır: "Bu komut, hizmetin önyükleme geliştirmeye yardımcı olması için bir başlangıç ​​noktası oluşturur - özellikle tasarım değiştiğinde yeniden çalıştırılması DEĞİLDİR."

Şimdi kodunuz şöyle görünecek:

Go'da API Geliştirme: cmd klasörü

Burada client.go , hem get hem de show yöntemlerinin sahte bir uygulamasına sahip örnek bir dosyadır. Hadi buna biraz iş mantığı ekleyelim!

Basit olması için, bellek içi veritabanı yerine SQLite ve ORM'miz olarak Gorm kullanacağız. sqlite.go dosyasını oluşturun ve aşağıdaki içeriği ekleyin - bu, veritabanında kayıtlar oluşturmak ve veritabanından bir ve/veya birçok satırı listelemek için veritabanı mantığı ekleyecektir:

 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 }

Ardından, client.go'yu client Service'deki tüm yöntemleri güncellemek, veritabanı çağrılarını uygulamak ve API yanıtlarını oluşturmak için düzenleriz:

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

Uygulamamızın ilk kesimi derlenmeye hazır. Sunucu ve istemci ikili dosyaları oluşturmak için aşağıdaki komutu çalıştırın:

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

Sunucuyu çalıştırmak için ./clients çalıştırmanız yeterlidir. Şimdilik çalışır durumda bırakın. Aşağıdaki gibi başarılı bir şekilde çalıştığını görmelisiniz:

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

Uygulamamızda bazı testler yapmaya hazırız. Cli kullanarak tüm yöntemleri deneyelim:

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

Herhangi bir hata alırsanız, SQLite ORM mantığının iyi olduğundan ve veritabanı başlatılmamış veya satır döndürmeyen sorgular gibi herhangi bir veritabanı hatasıyla karşılaşmadığınızdan emin olmak için sunucu günlüklerini kontrol edin.

API'nizi Genişletme

Çerçeve, API'nizi genişletmek ve kolayca daha fazla özellik eklemek için eklentilerin geliştirilmesini destekler. Goa, topluluk tarafından oluşturulan eklentiler için bir depoya sahiptir.

Daha önce açıkladığım gibi, geliştirme yaşam döngüsünün bir parçası olarak, tasarım tanımına geri dönerek, onu güncelleyerek ve oluşturulan kodumuzu yenileyerek uygulamamızı genişletmek için araç setine güvenebiliriz. API'ye CORS ve kimlik doğrulama ekleyerek eklentilerin nasıl yardımcı olabileceğini gösterelim.

clients/design/design.go dosyasını aşağıdaki içeriğe güncelleyin:

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

Yeni tasarımda iki büyük fark görebilirsiniz. Bir kullanıcının hizmeti çağırmaya yetkili olup olmadığını doğrulayabilmemiz için client hizmetinde bir güvenlik kapsamı tanımladık ve kullanıcıların kimliğini doğrulamak ve JSON Web Belirteçlerini (JWT) oluşturmak için kullanacağımız signin adlı ikinci bir hizmet tanımladık. client hizmeti aramaları yetkilendirmek için kullanacaktır. Ayrıca özel istemci Türümüze daha fazla alan ekledik. Bu, bir API geliştirirken yaygın bir durumdur—verileri yeniden şekillendirme veya yeniden yapılandırma ihtiyacı.

Tasarımda, bu değişiklikler kulağa basit gelebilir, ancak bunlar hakkında düşünüldüğünde, tasarımda açıklananları elde etmek için gereken çok sayıda minimal özellik vardır. Örneğin, API yöntemlerimizi kullanarak kimlik doğrulama ve yetkilendirme için mimari şemaları alın:

Go'da API Geliştirme: kimlik doğrulama ve yetkilendirme için mimari şemalar

Bunların hepsi henüz kodumuzun sahip olmadığı yeni özelliklerdir. Yine, Goa'nın geliştirme çabalarınıza daha fazla değer kattığı yer burasıdır. Aşağıdaki komutla kaynak kodunu yeniden oluşturarak bu özellikleri taşıma tarafında uygulayalım:

 goa gen clients/design

Bu noktada, Git'i kullanıyorsanız, yeni dosyaların varlığını ve diğerlerinin güncellenmiş olarak gösterildiğini fark edeceksiniz. Bunun nedeni, Goa'nın bizim müdahalemiz olmadan standart kodu buna göre sorunsuz bir şekilde yenilemesidir.

Şimdi servis tarafı kodunu uygulamamız gerekiyor. Gerçek dünyadaki bir uygulamada, tüm tasarım değişikliklerini yansıtmak için kaynağınızı güncelledikten sonra uygulamayı manuel olarak güncellersiniz. Goa'nın ilerlememizi önerdiği yol budur, ancak kısaca, bizi oraya daha hızlı götürmek için örnek uygulamayı silip yeniden oluşturacağım. Örnek uygulamayı silmek ve yeniden oluşturmak için aşağıdaki komutları çalıştırın:

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

Bununla, kodunuz aşağıdaki gibi görünmelidir:

Go'da API Geliştirme: yeniden oluşturma

Örnek uygulamamızda yeni bir dosya görebiliriz: login servis mantığını içeren signin.go . Ancak, client.go'nun belirteçleri doğrulamak için bir client.go işleviyle de güncellendiğini görebiliriz. Bu, tasarımda yazdıklarımızla eşleşir, bu nedenle istemcideki herhangi bir yönteme yapılan her çağrı, belirteç doğrulaması için durdurulacak ve yalnızca geçerli bir belirteç ve doğru bir kapsam tarafından yetkilendirilmişse iletilecektir.

Bu nedenle, API'nin kimliği doğrulanmış kullanıcılar için oluşturacağı belirteçleri oluşturmak için mantığı eklemek için signin.go içindeki oturum açma Hizmetimizde yöntemleri güncelleyeceğiz. Aşağıdaki bağlamı kopyalayıp signin.go yapıştırın:

 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 }

Son olarak, özel türümüze daha fazla alan eklediğimiz için, bu değişiklikleri yansıtmak için client.go client Service'deki Add yöntemini güncellememiz gerekiyor. client.go güncellemek için aşağıdakileri kopyalayıp yapıştırın:

 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 }

Ve bu kadar! Uygulamayı yeniden derleyip tekrar test edelim. Eski ikili dosyaları kaldırmak ve yenilerini derlemek için aşağıdaki komutları çalıştırın:

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

./clients tekrar çalıştırın ve çalışır durumda bırakın. Başarılı bir şekilde çalıştığını görmelisiniz, ancak bu sefer uygulanan yeni yöntemlerle:

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

Test etmek için, tüm API yöntemlerini cli kullanarak çalıştıralım; sabit kodlanmış kimlik bilgilerini kullandığımıza dikkat edin:

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

Ve işte başlıyoruz! Uygun kimlik doğrulama, kapsam yetkilendirme ve evrimsel büyüme için yer içeren minimalist bir uygulamamız var. Bundan sonra, bulut hizmetlerini veya seçtiğiniz herhangi bir başka kimlik sağlayıcısını kullanarak kendi kimlik doğrulama stratejinizi geliştirebilirsiniz. Ayrıca tercih ettiğiniz veritabanı veya mesajlaşma sistemi için eklentiler oluşturabilir, hatta diğer API'lerle kolayca entegre edebilirsiniz.

Daha fazla eklenti, örnek (çerçevenin belirli özelliklerini gösteren) ve diğer faydalı kaynaklar için Goa'nın GitHub projesine göz atın.

Bugünlük bu kadar. Umarım Goa ile oynamaktan ve bu makaleyi okumaktan keyif almışsınızdır. İçerikle ilgili herhangi bir geri bildiriminiz varsa GitHub, Twitter veya LinkedIn üzerinden bize ulaşmaktan çekinmeyin.

Ayrıca, Gophers Slack'teki #goa kanalında takılıyoruz, o yüzden gelin ve merhaba deyin!

Golang hakkında daha fazla bilgi için bkz. Go Programlama Dili: Bir Giriş Golang Eğitimi ve İyi Yapılandırılmış Mantık: Bir Golang OOP Eğitimi.