Pengembangan API di Go Menggunakan Goa
Diterbitkan: 2022-03-11Pengembangan API adalah topik hangat saat ini. Ada banyak cara untuk mengembangkan dan memberikan API, dan perusahaan besar telah mengembangkan solusi besar untuk membantu Anda mem-bootstrap aplikasi dengan cepat.
Namun, sebagian besar opsi tersebut tidak memiliki fitur utama: manajemen siklus hidup pengembangan. Jadi, pengembang menghabiskan beberapa siklus untuk membuat API yang berguna dan kuat tetapi akhirnya berjuang dengan evolusi organik yang diharapkan dari kode mereka dan implikasi yang dimiliki oleh perubahan kecil pada API dalam kode sumber.
Pada tahun 2016, Raphael Simon membuat Goa, sebuah kerangka kerja untuk pengembangan API di Golang dengan siklus hidup yang mengutamakan desain API. Di Goa, definisi API Anda tidak hanya dijelaskan sebagai kode tetapi juga sumber dari mana kode server, kode klien, dan dokumentasi berasal. Ini berarti bahwa kode Anda dijelaskan dalam definisi API Anda menggunakan Golang Domain Specific Language (DSL), kemudian dibuat menggunakan goa cli, dan diimplementasikan secara terpisah dari kode sumber aplikasi Anda.
Itulah alasan mengapa Goa bersinar. Ini adalah solusi dengan kontrak siklus hidup pengembangan yang terdefinisi dengan baik yang bergantung pada praktik terbaik saat menghasilkan kode (seperti memisahkan domain dan masalah yang berbeda dalam beberapa lapisan, sehingga aspek transportasi tidak mengganggu aspek bisnis aplikasi), mengikuti pola arsitektur bersih yang dapat dikomposisi modul dihasilkan untuk lapisan transport, endpoint, dan logika bisnis dalam aplikasi Anda.
Beberapa fitur Goa, seperti yang didefinisikan oleh situs resminya, meliputi:
- Komposit . Paket, algoritme pembuatan kode, dan kode yang dihasilkan semuanya bersifat modular.
- Transportasi-agnostik . Pemisahan lapisan transport dari implementasi layanan yang sebenarnya berarti bahwa layanan yang sama dapat mengekspos titik akhir yang dapat diakses melalui beberapa transportasi seperti HTTP dan/atau gRPC.
- Pemisahan kekhawatiran . Implementasi layanan yang sebenarnya diisolasi dari kode transportasi.
- Penggunaan jenis perpustakaan standar Go . Ini membuatnya lebih mudah untuk berinteraksi dengan kode eksternal.
Dalam artikel ini, saya akan membuat aplikasi dan memandu Anda melalui tahapan siklus hidup pengembangan API. Aplikasi mengelola detail tentang klien, seperti nama, alamat, nomor telepon, media sosial, dll. Pada akhirnya, kami akan mencoba memperluasnya dan menambahkan fitur baru untuk menjalankan siklus pengembangannya.
Jadi, mari kita mulai!
Mempersiapkan Area Pengembangan Anda
Langkah pertama kami adalah memulai repositori dan mengaktifkan dukungan modul Go:
mkdir -p clients/design cd clients go mod init clients
Pada akhirnya, struktur repo Anda harus seperti di bawah ini:
$ tree . ├── design └── go.mod
Mendesain API Anda
Sumber kebenaran untuk API Anda adalah definisi desain Anda. Seperti yang dinyatakan dalam dokumentasi, “Goa memungkinkan Anda memikirkan API Anda secara independen dari masalah implementasi apa pun dan kemudian meninjau desain itu dengan semua pemangku kepentingan sebelum menulis implementasinya.” Ini berarti bahwa setiap elemen API didefinisikan di sini terlebih dahulu, sebelum kode aplikasi yang sebenarnya dibuat. Tapi cukup bicara!
Buka file clients/design/design.go
dan tambahkan konten di bawah ini:
/* 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") })
Hal pertama yang dapat Anda perhatikan adalah bahwa DSL di atas adalah sekumpulan fungsi Go yang dapat disusun untuk menggambarkan API layanan jarak jauh. Fungsi disusun menggunakan argumen fungsi anonim. Dalam fungsi DSL, kami memiliki subset fungsi yang tidak seharusnya muncul dalam fungsi lain, yang kami sebut DSL tingkat atas. Di bawah ini, Anda memiliki sebagian set fungsi DSL dan strukturnya:
Jadi, dalam desain awal kami, kami memiliki DSL tingkat atas API yang menjelaskan API klien kami, satu DSL tingkat atas layanan yang menjelaskan layanan API utama, clients
, dan menyajikan file angkuh API, dan dua jenis DSL tingkat atas untuk menggambarkan jenis tampilan objek yang digunakan dalam muatan transportasi.
Fungsi API
adalah DSL tingkat atas opsional yang mencantumkan properti global API seperti nama, deskripsi, dan juga satu atau beberapa server yang berpotensi mengekspos rangkaian layanan yang berbeda. Dalam kasus kami, satu server sudah cukup, tetapi Anda juga dapat melayani layanan yang berbeda di tingkat yang berbeda: pengembangan, pengujian, dan produksi, misalnya.
Fungsi Service
mendefinisikan sekelompok metode yang berpotensi memetakan ke sumber daya dalam transportasi. Layanan juga dapat menentukan respons kesalahan umum. Metode layanan dijelaskan menggunakan Method
. Fungsi ini mendefinisikan jenis metode payload (input) dan hasil (output). Jika Anda menghilangkan jenis muatan atau hasil, jenis bawaan Empty, yang dipetakan ke badan kosong di HTTP, akan digunakan.
Terakhir, fungsi Type
atau ResultType
mendefinisikan tipe yang ditentukan pengguna, perbedaan utamanya adalah tipe hasil juga mendefinisikan sekumpulan "tampilan".
Dalam contoh kami, kami menjelaskan API dan menjelaskan bagaimana seharusnya melayani, dan kami juga membuat yang berikut:
- Sebuah layanan yang disebut
clients
- Tiga metode:
add
(untuk membuat satu klien),get
(untuk mengambil satu klien), danshow
(untuk mendaftar semua klien) - Jenis kustom kita sendiri, yang akan berguna saat kita berintegrasi dengan database, dan tipe kesalahan yang disesuaikan
Sekarang aplikasi kita telah dijelaskan, kita dapat menghasilkan kode boilerplate. Perintah berikut mengambil jalur impor paket desain sebagai argumen. Itu juga menerima jalur ke direktori keluaran sebagai bendera opsional:
goa gen clients/design
Perintah mengeluarkan nama file yang dihasilkannya. Di sana, direktori gen
berisi subdirektori nama aplikasi yang menampung kode layanan transport-independen. Subdirektori http
menjelaskan transport HTTP (kami memiliki kode server dan klien dengan logika untuk menyandikan dan mendekode permintaan dan tanggapan, dan kode CLI untuk membuat permintaan HTTP dari baris perintah). Ini juga berisi file spesifikasi Open API 2.0 dalam format JSON dan YAML.
Anda dapat menyalin konten file swagger dan menempelkannya ke editor Swagger online mana pun (seperti yang ada di swagger.io) untuk memvisualisasikan dokumentasi spesifikasi API Anda. Mereka mendukung format YAML dan JSON.
Kami sekarang siap untuk langkah selanjutnya dalam siklus hidup pengembangan.
Menerapkan API Anda
Setelah kode boilerplate Anda dibuat, saatnya menambahkan beberapa logika bisnis ke dalamnya. Pada titik ini, beginilah tampilan kode Anda:
Di mana setiap file di atas dipelihara dan diperbarui oleh Goa setiap kali kami menjalankan CLI. Jadi, saat arsitektur berkembang, desain Anda akan mengikuti evolusi, dan begitu juga kode sumber Anda. Untuk mengimplementasikan aplikasi, kami menjalankan perintah di bawah ini (ini akan menghasilkan implementasi dasar layanan bersama dengan file server yang dapat dibangun yang memutar goroutine untuk memulai server HTTP dan file klien yang dapat membuat permintaan ke server itu):
goa example clients/design
Ini akan menghasilkan folder cmd dengan sumber server dan klien yang dapat dibangun. Akan ada aplikasi Anda, dan itu adalah file yang harus Anda pertahankan sendiri setelah Goa pertama kali membuatnya.
Dokumentasi Goa menjelaskan bahwa: "Perintah ini menghasilkan titik awal bagi layanan untuk membantu pengembangan bootstrap - khususnya TIDAK dimaksudkan untuk dijalankan kembali ketika desain berubah."
Sekarang, kode Anda akan terlihat seperti:
Di mana client.go
adalah file contoh dengan implementasi dummy dari metode get
dan show
. Mari tambahkan beberapa logika bisnis ke dalamnya!
Untuk kesederhanaan, kami akan menggunakan SQLite sebagai ganti database dalam memori dan Gorm sebagai ORM kami. Buat file sqlite.go
dan tambahkan konten di bawah ini - yang akan menambahkan logika basis data untuk membuat catatan pada basis data dan mencantumkan satu dan/atau banyak baris dari basis data:
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 }
Kemudian, kami mengedit client.go untuk memperbarui semua metode di Layanan klien, mengimplementasikan panggilan database, dan membuat respons 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 }
Potongan pertama dari aplikasi kita siap untuk dikompilasi. Jalankan perintah berikut untuk membuat binari server dan klien:
go build ./cmd/clients go build ./cmd/clients-cli
Untuk menjalankan server, jalankan saja ./clients
. Biarkan berjalan untuk saat ini. Anda akan melihatnya berjalan dengan sukses, seperti berikut ini:
$ ./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"
Kami siap untuk melakukan beberapa pengujian di aplikasi kami. Mari kita coba semua metode menggunakan 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" } ]
Jika Anda mendapatkan kesalahan, periksa log server untuk memastikan bahwa logika SQLite ORM baik dan Anda tidak menghadapi kesalahan database seperti database tidak diinisialisasi atau kueri tidak mengembalikan baris.
Memperluas API Anda
Kerangka kerja ini mendukung pengembangan plugin untuk memperluas API Anda dan menambahkan lebih banyak fitur dengan mudah. Goa memiliki repositori untuk plugin yang dibuat oleh komunitas.
Seperti yang saya jelaskan sebelumnya, sebagai bagian dari siklus hidup pengembangan, kita dapat mengandalkan perangkat untuk memperluas aplikasi kita dengan kembali ke definisi desain, memperbaruinya, dan menyegarkan kode yang kita buat. Mari tunjukkan bagaimana plugin dapat membantu dengan menambahkan CORS dan otentikasi ke API.
Perbarui file clients/design/design.go
ke konten di bawah ini:
/* 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) }) }) })
Anda dapat melihat dua perbedaan utama dalam desain baru. Kami mendefinisikan ruang lingkup keamanan di layanan client
sehingga kami dapat memvalidasi jika pengguna berwenang untuk memanggil layanan, dan kami mendefinisikan layanan kedua yang disebut signin
, yang akan kami gunakan untuk mengautentikasi pengguna dan menghasilkan JSON Web Tokens (JWT), yang layanan client
akan digunakan untuk mengotorisasi panggilan. Kami juga telah menambahkan lebih banyak bidang ke Jenis klien khusus kami. Ini adalah kasus umum saat mengembangkan API—kebutuhan untuk membentuk kembali atau merestrukturisasi data.

Pada desain, perubahan ini mungkin terdengar sederhana, tetapi mengingatnya, ada banyak fitur minimal yang diperlukan untuk mencapai apa yang dijelaskan pada desain. Ambil, misalnya, skema arsitektur untuk otentikasi dan otorisasi menggunakan metode API kami:
Itu semua adalah fitur baru yang belum dimiliki kode kami. Sekali lagi, di sinilah Goa menambahkan nilai lebih pada upaya pengembangan Anda. Mari kita implementasikan fitur-fitur ini di sisi transport dengan membuat kembali kode sumber dengan perintah di bawah ini:
goa gen clients/design
Pada titik ini, jika Anda menggunakan Git, Anda akan melihat adanya file baru, dengan yang lain ditampilkan sebagai yang diperbarui. Ini karena Goa memperbarui kode boilerplate dengan mulus, tanpa campur tangan kami.
Sekarang, kita perlu mengimplementasikan kode sisi layanan. Dalam aplikasi dunia nyata, Anda akan memperbarui aplikasi secara manual setelah memperbarui sumber Anda untuk mencerminkan semua perubahan desain. Ini adalah cara yang direkomendasikan Goa untuk melanjutkan, tetapi untuk singkatnya, saya akan menghapus dan membuat ulang contoh aplikasi untuk membawa kita ke sana lebih cepat. Jalankan perintah di bawah ini untuk menghapus contoh aplikasi dan membuatnya kembali:
rm -rf cmd client.go goa example clients/design
Dengan itu, kode Anda akan terlihat seperti berikut:
Kita dapat melihat satu file baru di aplikasi contoh kita: signin.go
, yang berisi logika layanan masuk. Namun, kita dapat melihat bahwa client.go
juga diperbarui dengan fungsi JWTAuth untuk memvalidasi token. Ini cocok dengan apa yang telah kami tulis dalam desain, sehingga setiap panggilan ke metode apa pun di klien akan dicegat untuk validasi token dan diteruskan hanya jika disahkan oleh token yang valid dan cakupan yang benar.
Oleh karena itu, kami akan memperbarui metode di Layanan masuk kami di dalam signin.go
untuk menambahkan logika untuk menghasilkan token yang akan dibuat API untuk pengguna yang diautentikasi. Salin dan tempel konteks berikut ke 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 }
Terakhir, karena kami menambahkan lebih banyak bidang ke jenis kustom kami, kami perlu memperbarui metode Tambahkan pada Layanan klien di client.go
untuk mencerminkan perubahan tersebut. Salin dan tempel berikut ini untuk memperbarui client.go
Anda :
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 }
Dan itu saja! Mari kita kompilasi ulang aplikasi dan uji lagi. Jalankan perintah di bawah ini untuk menghapus binari lama dan mengkompilasi yang baru:
rm -f clients clients-cli go build ./cmd/clients go build ./cmd/clients-cli
Jalankan ./clients
lagi dan biarkan berjalan. Anda akan melihatnya berjalan dengan sukses, tetapi kali ini, dengan metode baru yang diterapkan:
$ ./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"
Untuk menguji, mari jalankan semua metode API menggunakan cli—perhatikan bahwa kita menggunakan kredensial hardcode:
$ ./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 } ]
Dan di sana kita pergi! Kami memiliki aplikasi minimalis dengan otentikasi yang tepat, otorisasi ruang lingkup, dan ruang untuk pertumbuhan evolusioner. Setelah ini, Anda dapat mengembangkan strategi otentikasi Anda sendiri menggunakan layanan cloud atau penyedia identitas lain pilihan Anda. Anda juga dapat membuat plugin untuk database atau sistem perpesanan pilihan Anda, atau bahkan berintegrasi dengan API lain dengan mudah.
Lihat proyek GitHub Goa untuk lebih banyak plugin, contoh (menunjukkan kemampuan spesifik dari kerangka kerja), dan sumber daya berguna lainnya.
Itu saja untuk hari ini. Saya harap Anda menikmati bermain dengan Goa dan membaca artikel ini. Jika Anda memiliki umpan balik tentang konten, jangan ragu untuk menghubungi GitHub, Twitter, atau LinkedIn.
Juga, kami nongkrong di saluran #goa di Gophers Slack, jadi datanglah dan sapa!
Untuk informasi lebih lanjut tentang Golang, lihat Bahasa Pemrograman Go: Tutorial Pengenalan Golang dan Logika Terstruktur Baik: Tutorial OOP Golang.