Goa를 사용한 Go API 개발

게시 됨: 2022-03-11

API 개발은 요즘 화두입니다. API를 개발하고 제공할 수 있는 방법은 매우 다양하며 대기업은 애플리케이션을 신속하게 부트스트랩하는 데 도움이 되는 대규모 솔루션을 개발했습니다.

그러나 이러한 옵션의 대부분에는 개발 수명 주기 관리라는 핵심 기능이 부족합니다. 따라서 개발자는 유용하고 강력한 API를 만드는 데 약간의 시간을 할애하지만 결국 코드의 예상되는 유기적 발전과 API의 작은 변경이 소스 코드에 미치는 영향으로 어려움을 겪습니다.

2016년에 Raphael Simon은 API 디자인을 최우선으로 하는 라이프사이클을 사용하여 Golang에서 API 개발을 위한 프레임워크인 Goa를 만들었습니다. Goa에서 API 정의는 코드로 설명될 뿐만 아니라 서버 코드, 클라이언트 코드 및 문서가 파생되는 소스이기도 합니다. 이는 코드가 Golang DSL(Domain Specific Language)을 사용하여 API 정의에 설명된 다음 goa cli를 사용하여 생성되고 애플리케이션 소스 코드와 별도로 구현됨을 의미합니다.

고아가 빛나는 이유다. 코드를 생성할 때 모범 사례에 의존하는 잘 정의된 개발 수명 주기 계약이 있는 솔루션입니다(예: 전송 측면이 애플리케이션의 비즈니스 측면을 방해하지 않도록 다양한 도메인 및 문제 분할). 구성 가능한 깨끗한 아키텍처 패턴을 따릅니다. 모듈은 애플리케이션의 전송, 끝점 및 비즈니스 논리 계층에 대해 생성됩니다.

공식 웹사이트에서 정의한 일부 Goa 기능은 다음과 같습니다.

  • 구성성 . 패키지, 코드 생성 알고리즘 및 생성된 코드는 모두 모듈식입니다.
  • 운송 불가지론 . 실제 서비스 구현에서 전송 계층의 분리는 동일한 서비스가 HTTP 및/또는 gRPC와 같은 다중 전송을 통해 액세스할 수 있는 끝점을 노출할 수 있음을 의미합니다.
  • 우려의 분리 . 실제 서비스 구현은 전송 코드와 분리됩니다.
  • Go 표준 라이브러리 유형 사용 . 이렇게 하면 외부 코드와 더 쉽게 인터페이스할 수 있습니다.

이 기사에서는 애플리케이션을 만들고 API 개발 수명 주기의 단계를 안내합니다. 응용 프로그램은 이름, 주소, 전화 번호, 소셜 미디어 등과 같은 클라이언트에 대한 세부 정보를 관리합니다. 결국 우리는 확장을 시도하고 개발 수명 주기를 실행하기 위해 새로운 기능을 추가할 것입니다.

시작하겠습니다!

개발 영역 준비

첫 번째 단계는 저장소를 시작하고 Go 모듈 지원을 활성화하는 것입니다.

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

결국 repo 구조는 다음과 같아야 합니다.

 $ 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 스웨거 파일을 제공하는 서비스 최상위 DSL, 설명을 위한 두 가지 유형의 최상위 DSL이 있습니다. 전송 페이로드에 사용되는 객체 보기 유형입니다.

API 기능은 이름, 설명 및 잠재적으로 다른 서비스 집합을 노출하는 하나 이상의 서버와 같은 API의 전역 속성을 나열하는 선택적 최상위 수준 DSL입니다. 우리의 경우 하나의 서버로 충분하지만 개발, 테스트 및 프로덕션과 같은 다양한 계층에서 다양한 서비스를 제공할 수도 있습니다.

Service 기능은 잠재적으로 전송의 리소스에 매핑되는 메서드 그룹을 정의합니다. 서비스는 일반적인 오류 응답을 정의할 수도 있습니다. 서비스 방법은 Method 를 사용하여 설명됩니다. 이 함수는 메소드 페이로드(입력) 및 결과(출력) 유형을 정의합니다. 페이로드 또는 결과 유형을 생략하면 HTTP의 빈 본문에 매핑되는 기본 제공 유형 Empty가 사용됩니다.

마지막으로 Type 또는 ResultType 함수는 사용자 정의 유형을 정의하며, 주요 차이점은 결과 유형도 "보기" 세트를 정의한다는 것입니다.

이 예에서 우리는 API를 설명하고 제공하는 방법을 설명했으며 다음도 만들었습니다.

  • clients 라는 서비스
  • 세 가지 방법: add (하나의 클라이언트 생성용), get (하나의 클라이언트 검색용), show (모든 클라이언트 나열용)
  • 데이터베이스와 통합할 때 유용할 자체 사용자 정의 유형 및 사용자 정의 오류 유형

이제 애플리케이션이 설명되었으므로 상용구 코드를 생성할 수 있습니다. 다음 명령은 디자인 패키지 가져오기 경로를 인수로 사용합니다. 또한 출력 디렉토리에 대한 경로를 선택적 플래그로 허용합니다.

 goa gen clients/design

이 명령은 생성하는 파일의 이름을 출력합니다. 거기에서 gen 디렉토리에는 전송 독립적인 서비스 코드가 있는 응용 프로그램 이름 하위 디렉토리가 있습니다. http 하위 디렉토리는 HTTP 전송을 설명합니다(요청 및 응답을 인코딩 및 디코딩하는 논리가 있는 서버 및 클라이언트 코드와 명령줄에서 HTTP 요청을 빌드하는 CLI 코드). 또한 JSON 및 YAML 형식의 Open API 2.0 사양 파일도 포함합니다.

API 사양 문서를 시각화하기 위해 swagger 파일의 내용을 복사하여 온라인 Swagger 편집기(swagger.io에 있는 것과 같은)에 붙여넣을 수 있습니다. YAML 및 JSON 형식을 모두 지원합니다.

이제 개발 수명 주기의 다음 단계를 수행할 준비가 되었습니다.

API 구현

상용구 코드를 만든 후에는 여기에 몇 가지 비즈니스 로직을 추가할 차례입니다. 이 시점에서 코드는 다음과 같아야 합니다.

Go에서 API 개발

CLI를 실행할 때마다 Goa에서 위의 모든 파일을 유지 관리하고 업데이트합니다. 따라서 아키텍처가 발전함에 따라 디자인도 발전하고 소스 코드도 발전할 것입니다. 애플리케이션을 구현하기 위해 아래 명령을 실행합니다(이는 HTTP 서버를 시작하기 위해 고루틴을 실행하는 빌드 가능한 서버 파일 및 해당 서버에 요청할 수 있는 클라이언트 파일과 함께 서비스의 기본 구현을 생성합니다):

 goa example clients/design

그러면 서버 및 클라이언트 빌드 가능한 소스가 모두 있는 cmd 폴더가 생성됩니다. 당신의 애플리케이션이 있을 것이고, 이것들은 Goa가 파일을 처음 생성한 후 유지해야 하는 파일입니다.

Goa 문서는 "이 명령은 부트스트랩 개발을 돕기 위한 서비스의 시작점을 생성합니다. 특히 디자인이 변경될 때 다시 실행하도록 의도되지 않았습니다."라고 명시하고 있습니다.

이제 코드는 다음과 같습니다.

Go에서 API 개발: cmd 폴더

여기서 client.gogetshow 메소드의 더미 구현이 있는 예제 파일입니다. 여기에 비즈니스 로직을 추가해 봅시다!

단순화를 위해 인메모리 데이터베이스 대신 SQLite를 사용하고 ORM으로 Gorm을 사용합니다. 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를 확장하고 더 많은 기능을 쉽게 추가할 수 있는 플러그인 개발을 지원합니다. 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) }) }) })

새로운 디자인에서 두 가지 주요 차이점을 확인할 수 있습니다. 사용자가 서비스를 호출할 권한이 있는지 확인할 수 있도록 client 서비스에 보안 범위를 정의했으며, 사용자를 인증하고 JSON 웹 토큰(JWT)을 생성하는 데 사용할 두 번째 서비스 signin 을 정의했습니다. 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 새로운 파일을 볼 수 있습니다. 그러나 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 프로젝트를 확인하세요.

오늘은 여기까지입니다. Go와 함께 플레이하고 이 기사를 읽는 것이 즐거웠기를 바랍니다. 콘텐츠에 대한 피드백이 있는 경우 GitHub, Twitter 또는 LinkedIn에서 언제든지 연락하세요.

또한 Gophers Slack의 #goa 채널에서 시간을 보내므로 방문하여 인사를 나누세요!

Golang에 대한 자세한 내용은 Go 프로그래밍 언어: Golang 입문 자습서 및 잘 구조화된 논리: Golang OOP 자습서를 참조하세요.