Разработка API на Go с помощью Goa

Опубликовано: 2022-03-11

Разработка API — горячая тема в наши дни. Существует огромное количество способов разработки и доставки API, и крупные компании разработали масштабные решения, которые помогут вам быстро запустить приложение.

Тем не менее, в большинстве этих вариантов отсутствует ключевая функция: управление жизненным циклом разработки. Таким образом, разработчики тратят несколько циклов на создание полезных и надежных API-интерфейсов, но в конечном итоге сталкиваются с ожидаемой органической эволюцией своего кода и последствиями, которые небольшие изменения в API имеют в исходном коде.

В 2016 году Рафаэль Саймон создал Goa, фреймворк для разработки API на Golang с жизненным циклом, который ставит дизайн API на первое место. В Гоа ваше определение API не только описывается как код, но также является источником, из которого получают серверный код, клиентский код и документацию. Это означает, что ваш код описан в вашем определении API с использованием доменного языка Golang (DSL), затем сгенерирован с помощью goa cli и реализован отдельно от исходного кода вашего приложения.

Вот почему Гоа сияет. Это решение с четко определенным контрактом жизненного цикла разработки, основанным на передовых методах создания кода (например, разделение различных доменов и проблем на уровни, чтобы транспортные аспекты не мешали бизнес-аспектам приложения), следуя шаблону чистой архитектуры, где возможно компонование. модули создаются для уровней транспорта, конечной точки и бизнес-логики в вашем приложении.

Некоторые особенности Гоа, как определено на официальном сайте, включают:

  • Компонуемость . Пакет, алгоритмы генерации кода и сгенерированный код являются модульными.
  • Транспорт-агностик . Отделение транспортного уровня от фактической реализации службы означает, что одна и та же служба может предоставлять конечные точки, доступные через несколько транспортов, таких как HTTP и/или gRPC.
  • Разделение забот . Фактическая реализация службы изолирована от транспортного кода.
  • Использование типов стандартной библиотеки Go . Это упрощает взаимодействие с внешним кодом.

В этой статье я создам приложение и проведу вас через этапы жизненного цикла разработки API. Приложение управляет сведениями о клиентах, такими как имя, адрес, номер телефона, социальные сети и т. д. В конце концов, мы попытаемся расширить его и добавить новые функции для осуществления его жизненного цикла разработки.

Итак, приступим!

Подготовка области разработки

Наш первый шаг — инициировать репозиторий и включить поддержку модулей Go:

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

В конце концов, ваша структура репо должна быть такой, как показано ниже:

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

Разработка вашего API

Источником истины для вашего API является ваше определение дизайна. Как говорится в документации, «Goa позволяет вам думать о ваших API независимо от каких-либо проблем с реализацией, а затем просматривать этот дизайн со всеми заинтересованными сторонами, прежде чем писать реализацию». Это означает, что здесь сначала определяется каждый элемент API, прежде чем будет сгенерирован фактический код приложения. Но хватит болтать!

Откройте файл clients/design/design.go и добавьте следующее содержимое:

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

Первое, что вы можете заметить, это то, что приведенный выше DSL представляет собой набор функций Go, которые можно составить для описания API удаленной службы. Функции составлены с использованием аргументов анонимных функций. В функциях DSL у нас есть подмножество функций, которые не должны появляться внутри других функций, которые мы называем DSL верхнего уровня. Ниже у вас есть неполный набор функций DSL и их структура:

DSL-функции

Итак, в нашем первоначальном проекте у нас есть DSL верхнего уровня API, описывающий API нашего клиента, один DSL верхнего уровня службы, описывающий основную службу API, clients и обслуживающий файл API, и два DSL верхнего уровня типа для описания тип представления объекта, используемый в полезной нагрузке транспорта.

Функция API — это необязательный DSL верхнего уровня, в котором перечислены глобальные свойства API, такие как имя, описание, а также один или несколько серверов, потенциально предоставляющих различные наборы услуг. В нашем случае достаточно одного сервера, но вы также можете обслуживать разные сервисы на разных уровнях: например, разработку, тестирование и производство.

Функция Service определяет группу методов, которые потенциально сопоставляются с ресурсом в транспорте. Служба также может определять общие ответы на ошибки. Методы обслуживания описываются с помощью Method . Эта функция определяет типы полезной нагрузки (ввод) и результата (выход) метода. Если вы опускаете тип полезной нагрузки или результата, используется встроенный тип Empty, который сопоставляется с пустым телом в HTTP.

Наконец, функции Type или ResultType определяют определяемые пользователем типы, основное отличие которых состоит в том, что тип результата также определяет набор «представлений».

В нашем примере мы описали API и объяснили, как он должен работать, а также создали следующее:

  • Служба под названием clients
  • Три метода: add (для создания одного клиента), get (для извлечения одного клиента) и show (для вывода списка всех клиентов).
  • Наши собственные пользовательские типы, которые пригодятся при интеграции с базой данных, и настраиваемый тип ошибки

Теперь, когда наше приложение описано, мы можем сгенерировать шаблонный код. Следующая команда принимает путь импорта пакета дизайна в качестве аргумента. Он также принимает путь к выходному каталогу в качестве необязательного флага:

 goa gen clients/design

Команда выводит имена файлов, которые она генерирует. Там каталог gen содержит подкаталог имени приложения, в котором находится независимый от транспорта код службы. Подкаталог http описывает транспорт HTTP (у нас есть серверный и клиентский код с логикой для кодирования и декодирования запросов и ответов, а также код CLI для создания HTTP-запросов из командной строки). Он также содержит файлы спецификаций Open API 2.0 в форматах JSON и YAML.

Вы можете скопировать содержимое файла swagger и вставить его в любой онлайн-редактор Swagger (например, на swagger.io) для визуализации документации по спецификации API. Они поддерживают форматы YAML и JSON.

Теперь мы готовы к следующему шагу в жизненном цикле разработки.

Внедрение вашего API

После того, как ваш шаблонный код был создан, пришло время добавить к нему некоторую бизнес-логику. На данный момент ваш код должен выглядеть так:

Разработка API в Go

Где каждый файл выше поддерживается и обновляется Гоа всякий раз, когда мы запускаем CLI. Таким образом, по мере развития архитектуры ваш дизайн будет следовать эволюции, как и ваш исходный код. Чтобы реализовать приложение, мы выполняем приведенную ниже команду (она создаст базовую реализацию службы вместе со сборными серверными файлами, которые запускают горутины для запуска HTTP-сервера, и клиентские файлы, которые могут отправлять запросы к этому серверу):

 goa example clients/design

Это создаст папку cmd с исходными кодами как для сервера, так и для клиента. Там будет ваше приложение, и это файлы, которые вы должны поддерживать самостоятельно после того, как Goa впервые сгенерирует их.

В документации Goa четко указано, что: «Эта команда создает отправную точку для службы, чтобы помочь начальной разработке — в частности, она НЕ предназначена для повторного запуска при изменении дизайна».

Теперь ваш код будет выглядеть так:

Разработка API в Go: папка cmd

Где client.go — пример файла с фиктивной реализацией методов get и show . Давайте добавим к нему немного бизнес-логики!

Для простоты мы будем использовать SQLite вместо базы данных в памяти и Gorm в качестве нашей ORM. Создайте файл sqlite.go и добавьте содержимое ниже — это добавит логику базы данных для создания записей в базе данных и перечисления одной и/или нескольких строк из базы данных:

 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 }

Затем мы редактируем client.go, чтобы обновить все методы в клиентской службе, реализовав вызовы базы данных и создав ответы 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 }

Первая версия нашего приложения готова к компиляции. Выполните следующую команду, чтобы создать двоичные файлы сервера и клиента:

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

Чтобы запустить сервер, просто запустите ./clients . Оставьте его пока работать. Вы должны увидеть, что он работает успешно, как показано ниже:

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

Мы готовы выполнить некоторое тестирование в нашем приложении. Давайте попробуем все методы с помощью 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" } ]

Если вы получили какую-либо ошибку, проверьте журналы сервера, чтобы убедиться, что логика SQLite ORM работает правильно, и вы не сталкиваетесь с какими-либо ошибками базы данных, такими как база данных не инициализирована или запросы не возвращают строк.

Расширение вашего API

Фреймворк поддерживает разработку плагинов для расширения вашего API и легкого добавления дополнительных функций. В Гоа есть репозиторий плагинов, созданных сообществом.

Как я объяснял ранее, в рамках жизненного цикла разработки мы можем полагаться на набор инструментов для расширения нашего приложения, возвращаясь к определению проекта, обновляя его и обновляя сгенерированный код. Давайте продемонстрируем, как плагины могут помочь, добавив CORS и аутентификацию в API.

Обновите файл clients/design/design.go до следующего содержания:

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

Вы можете заметить два основных отличия в новом дизайне. Мы определили область безопасности в client службе, чтобы мы могли проверить, авторизован ли пользователь для вызова службы, и мы определили вторую службу с именем signin , которую мы будем использовать для аутентификации пользователей и создания веб-токенов JSON (JWT), которые client служба будет использовать для авторизации вызовов. Мы также добавили больше полей в наш пользовательский тип клиента. Это распространенный случай при разработке API — необходимость изменить форму или реструктурировать данные.

С точки зрения дизайна эти изменения могут показаться простыми, но если подумать о них, то для достижения того, что описано в дизайне, требуется множество минимальных функций. Возьмем, к примеру, архитектурные схемы для аутентификации и авторизации с использованием наших методов API:

Разработка API в Go: архитектурные схемы для аутентификации и авторизации

Это все новые функции, которых еще нет в нашем коде. Опять же, именно здесь Гоа повышает ценность ваших усилий по развитию. Давайте реализуем эти функции на транспортной стороне, повторно сгенерировав исходный код с помощью следующей команды:

 goa gen clients/design

На этом этапе, если вы используете Git, вы заметите наличие новых файлов, а другие отображаются как обновленные. Это связано с тем, что Гоа плавно обновил шаблонный код без нашего вмешательства.

Теперь нам нужно реализовать код на стороне службы. В реальном приложении вы будете вручную обновлять приложение после обновления исходного кода, чтобы отразить все изменения дизайна. Это то, как Гоа рекомендует действовать, но для краткости я буду удалять и заново создавать пример приложения, чтобы мы быстрее добрались до цели. Запустите приведенные ниже команды, чтобы удалить пример приложения и создать его заново:

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

При этом ваш код должен выглядеть следующим образом:

Разработка API в Go: регенерация

В нашем примере приложения мы видим один новый файл: signin.go , который содержит логику службы входа. Однако мы видим, что client.go также был обновлен функцией JWTAuth для проверки токенов. Это соответствует тому, что мы написали в дизайне, поэтому каждый вызов любого метода в клиенте будет перехватываться для проверки токена и перенаправляться только в том случае, если он авторизован действительным токеном и правильной областью действия.

Поэтому мы обновим методы в нашей службе входа внутри signin.go , чтобы добавить логику для создания токенов, которые API будет создавать для аутентифицированных пользователей. Скопируйте и вставьте следующий контекст в 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 }

Наконец, поскольку мы добавили больше полей в наш пользовательский тип, нам нужно обновить метод Add в клиентской службе в client.go , чтобы отразить такие изменения. Скопируйте и вставьте следующее, чтобы обновить ваш 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 }

Вот и все! Давайте перекомпилируем приложение и снова протестируем его. Запустите приведенные ниже команды, чтобы удалить старые двоичные файлы и скомпилировать новые:

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

Снова запустите ./clients и оставьте его запущенным. Вы должны увидеть, что он работает успешно, но на этот раз с новыми реализованными методами:

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

Для проверки давайте выполним все методы API с помощью cli — обратите внимание, что мы используем жестко заданные учетные данные:

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

И вот мы идем! У нас есть минималистское приложение с надлежащей аутентификацией, авторизацией области действия и возможностью эволюционного роста. После этого вы можете разработать собственную стратегию аутентификации, используя облачные сервисы или любого другого поставщика удостоверений по вашему выбору. Вы также можете создавать плагины для предпочитаемой вами базы данных или системы обмена сообщениями или даже легко интегрироваться с другими API.

Посетите проект Goa на GitHub, чтобы найти дополнительные плагины, примеры (показывающие конкретные возможности фреймворка) и другие полезные ресурсы.

Это все на сегодня. Надеюсь, вам понравилось играть с Гоа и читать эту статью. Если у вас есть какие-либо отзывы о содержании, не стесняйтесь обращаться к GitHub, Twitter или LinkedIn.

Кроме того, мы общаемся на канале #goa на Gophers Slack, так что заходите и скажите привет!

Для получения дополнительной информации о Golang см. Язык программирования Go: вводное руководство по Golang и хорошо структурированная логика: руководство по Golang OOP.