Goaを使用したGoでのAPI開発

公開: 2022-03-11

API開発は最近話題になっています。 APIを開発して提供する方法は多数あり、大企業はアプリケーションを迅速にブートストラップするのに役立つ大規模なソリューションを開発しています。

しかし、これらのオプションのほとんどには、開発ライフサイクル管理という重要な機能がありません。 そのため、開発者は有用で堅牢なAPIを作成するためにいくつかのサイクルを費やしますが、コードの予想される有機的な進化と、APIの小さな変更がソースコードに与える影響に苦労することになります。

2016年、Raphael Simonは、API設計を最優先するライフサイクルを備えたGolangでのAPI開発のフレームワークであるGoaを作成しました。 Goaでは、API定義はコードとして記述されるだけでなく、サーバーコード、クライアントコード、およびドキュメントが派生するソースでもあります。 つまり、コードはGolangドメイン固有言語(DSL)を使用してAPI定義で記述され、goa cliを使用して生成され、アプリケーションのソースコードとは別に実装されます。

それがゴアが輝く理由です。 これは、コードを生成する際のベストプラクティスに依存する、明確に定義された開発ライフサイクルコントラクトを備えたソリューションです(さまざまなドメインや懸念事項をレイヤーに分割するなど、トランスポートの側面がアプリケーションのビジネスの側面に干渉しないようにします)。モジュールは、アプリケーションのトランスポート、エンドポイント、およびビジネスロジックレイヤー用に生成されます。

公式ウェブサイトで定義されているGoaの機能には、次のものがあります。

  • 構成可能性。 パッケージ、コード生成アルゴリズム、および生成されたコードはすべてモジュール式です。
  • トランスポートにとらわれない。 トランスポート層を実際のサービス実装から切り離すことは、同じサービスが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は、リモートサービスAPIを記述するために構成できるGo関数のセットであるということです。 関数は無名関数の引数を使用して構成されます。 DSL関数には、他の関数内に表示されないはずの関数のサブセットがあり、これをトップレベルDSLと呼びます。 以下に、DSL関数とその構造の部分的なセットを示します。

DSL機能

したがって、初期設計では、クライアントのAPIを記述するAPIトップレベルDSL、主要なAPIサービス、 clientsを記述し、APIスワッガーファイルを提供する1つのサービストップレベルDSL、およびトランスポートペイロードで使用されるオブジェクトビュータイプ。

API関数はオプションのトップレベルDSLであり、名前、説明、およびさまざまなサービスのセットを公開する可能性のある1つ以上のサーバーなどのAPIのグローバルプロパティを一覧表示します。 この場合、1台のサーバーで十分ですが、開発、テスト、本番など、さまざまな層でさまざまなサービスを提供することもできます。

Service関数は、トランスポート内のリソースにマップされる可能性のあるメソッドのグループを定義します。 サービスは、一般的なエラー応答を定義する場合もあります。 サービスメソッドは、 Methodを使用して記述されます。 この関数は、メソッドのペイロード(入力)タイプと結果(出力)タイプを定義します。 ペイロードまたは結果タイプを省略すると、HTTPで空の本体にマップされる組み込みタイプEmptyが使用されます。

最後に、 Type関数またはResultType関数はユーザー定義型を定義します。主な違いは、結果型が一連の「ビュー」も定義することです。

この例では、APIについて説明し、それがどのように機能するかを説明しました。また、以下を作成しました。

  • clientsと呼ばれるサービス
  • 3つのメソッド: add (1つのクライアントを作成するため)、 get (1つのクライアントを取得するため)、およびshow (すべてのクライアントをリストするため)
  • データベースと統合するときに役立つ独自のカスタムタイプと、カスタマイズされたエラータイプ

アプリケーションについて説明したので、ボイラープレートコードを生成できます。 次のコマンドは、デザインパッケージのインポートパスを引数として取ります。 また、オプションのフラグとして出力ディレクトリへのパスを受け入れます。

 goa gen clients/design

このコマンドは、生成したファイルの名前を出力します。 そこでは、 genディレクトリに、トランスポートに依存しないサービスコードを格納するアプリケーション名サブディレクトリが含まれています。 httpサブディレクトリには、HTTPトランスポートが記述されています(要求と応答をエンコードおよびデコードするロジックを備えたサーバーとクライアントのコード、およびコマンドラインからHTTP要求を構築するためのCLIコードがあります)。 また、JSON形式とYAML形式の両方のOpenAPI2.0仕様ファイルも含まれています。

Swaggerファイルのコンテンツをコピーして、オンラインのSwaggerエディター(swagger.ioにあるエディターなど)に貼り付けて、API仕様のドキュメントを視覚化することができます。 YAML形式とJSON形式の両方をサポートします。

これで、開発ライフサイクルの次のステップの準備が整いました。

APIの実装

ボイラープレートコードが作成されたら、ビジネスロジックを追加します。 この時点で、コードは次のようになります。

GoでのAPI開発

上記のすべてのファイルは、CLIを実行するたびにGoaによって維持および更新されます。 したがって、アーキテクチャが進化するにつれて、設計は進化に従い、ソースコードも進化します。 アプリケーションを実装するには、以下のコマンドを実行します(サービスの基本的な実装と、ゴルーチンを起動してHTTPサーバーを起動するビルド可能なサーバーファイルと、そのサーバーにリクエストを送信できるクライアントファイルを生成します)。

 goa example clients/design

これにより、サーバーとクライアントの両方のビルド可能なソースを含むcmdフォルダーが生成されます。 アプリケーションがあり、それらはGoaが最初に生成した後に自分で維持する必要のあるファイルです。

Goaのドキュメントでは、次のように明確にされています。「このコマンドは、ブートストラップ開発を支援するサービスの開始点を生成します。特に、設計が変更されたときに再実行することを意図したものではありません。」

これで、コードは次のようになります。

GoでのAPI開発:cmdフォルダー

ここで、 client.goは、 getメソッドとshowメソッドの両方のダミー実装を含むサンプルファイルです。 それにビジネスロジックを追加しましょう!

簡単にするために、インメモリデータベースの代わりにSQLiteを使用し、ORMとしてGormを使用します。 ファイルsqlite.goを作成し、以下のコンテンツを追加します。これにより、データベースロジックが追加され、データベースにレコードが作成され、データベースから1つまたは複数の行が一覧表示されます。

 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を編集して、client Serviceのすべてのメソッドを更新し、データベース呼び出しを実装して、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を拡張し、機能を簡単に追加するためのプラグインの開発をサポートしています。 Goaには、コミュニティによって作成されたプラグインのリポジトリがあります。

前に説明したように、開発ライフサイクルの一部として、ツールセットを使用して、設計定義に戻り、更新し、生成されたコードを更新することで、アプリケーションを拡張できます。 APIにCORSと認証を追加することで、プラグインがどのように役立つかを紹介しましょう。

ファイル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) }) }) })

新しいデザインには2つの大きな違いがあります。 clientサービスでセキュリティスコープを定義して、ユーザーがサービスの呼び出しを許可されているかどうかを検証できるようにしました。また、ユーザーの認証とJSON Web Token(JWT)の生成に使用するsigninという2番目のサービスを定義しました。 clientサービスは、呼び出しを承認するために使用します。 また、カスタムクライアントタイプにさらにフィールドを追加しました。 これは、APIを開発する際の一般的なケースであり、データを再形成または再構築する必要があります。

設計上、これらの変更は単純に聞こえるかもしれませんが、それらを反映して、設計で説明されていることを実現するために必要な最小限の機能がたくさんあります。 たとえば、APIメソッドを使用した認証と承認のアーキテクチャ回路図を見てください。

GoでのAPI開発:認証と承認のためのアーキテクチャ回路図

これらはすべて、コードにまだない新機能です。 繰り返しになりますが、これはGoaが開発作業により多くの価値を追加する場所です。 以下のコマンドを使用してソースコードを再生成することにより、トランスポート側でこれらの機能を実装しましょう。

 goa gen clients/design

この時点で、Gitを使用している場合は、新しいファイルが存在し、他のファイルは更新されていることがわかります。 これは、Goaが介入なしに、それに応じてボイラープレートコードをシームレスに更新したためです。

次に、サービス側のコードを実装する必要があります。 実際のアプリケーションでは、すべての設計変更を反映するようにソースを更新した後、アプリケーションを手動で更新します。 これは、Goaが推奨する方法ですが、簡潔にするために、サンプルアプリケーションを削除して再生成し、より速くそこに到達できるようにします。 以下のコマンドを実行して、サンプルアプリケーションを削除し、再生成します。

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

これで、コードは次のようになります。

GoでのAPI開発:再生成

サンプルアプリケーションには、サインインサービスロジックを含むsignin.goという新しいファイルが1つあります。 ただし、 client.goもトークンを検証するためのJWTAuth関数で更新されていることがわかります。 これは、デザインで記述したものと一致するため、クライアントのメソッドへのすべての呼び出しは、トークンの検証のためにインターセプトされ、有効なトークンと正しいスコープによって承認された場合にのみ転送されます。

したがって、認証されたユーザーに対してAPIが作成するトークンを生成するロジックを追加するために、 signin.go内のサインインサービスのメソッドを更新します。 次のコンテキストをコピーして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 }

最後に、カスタムタイプにフィールドを追加したため、このような変更を反映するために、 client.goのクライアントサービスのAddメソッドを更新する必要があります。 以下をコピーして貼り付け、 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"

テストするために、cliを使用してすべてのAPIメソッドを実行しましょう。ハードコードされたクレデンシャルを使用していることに注意してください。

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

そして、そこに行きます! 適切な認証、スコープ認証、および進化的な成長の余地を備えた最小限のアプリケーションがあります。 この後、クラウドサービスまたは任意の他のIDプロバイダーを使用して独自の認証戦略を開発できます。 また、好みのデータベースやメッセージングシステム用のプラグインを作成したり、他のAPIと簡単に統合したりすることもできます。

その他のプラグイン、例(フレームワークの特定の機能を示す)、およびその他の有用なリソースについては、GoaのGitHubプロジェクトを確認してください。

今日は以上です。 ゴアで遊んだり、この記事を読んだりして楽しんでいただけたでしょうか。 コンテンツに関するフィードバックがある場合は、GitHub、Twitter、またはLinkedInでお気軽にご連絡ください。

また、Gophers Slackの#goaチャンネルにたむろしているので、是非お越しください。

Golangの詳細については、「Goプログラミング言語:入門用Golangチュートリアル」および「適切に構造化されたロジック:GolangOOPチュートリアル」を参照してください。