作者都是各自领域经过审查的专家,并撰写他们有经验的主题. All of our content is peer reviewed and validated by Toptal experts in the same field.
格莱德森·纳西门托的头像

Gleidson Nascimento

Gleidson是一位经验丰富的工程师,拥有基础设施自动化架构方面的技能, design, 发展, 和编制.

Expertise

以前在

IBM
Share

API开发是当今的一个热门话题. There’s a huge number of ways you can develop and deliver an API, 大公司已经开发了大量的解决方案来帮助您快速启动应用程序.

Yet, most of those options lack a key feature: 发展 lifecycle management. So, 开发人员花了一些周期来创建有用且健壮的API,但最终却要与预期的代码有机演变以及API在源代码中的微小变化所带来的影响作斗争.

In 2016, 拉斐尔西蒙 created Goa, API的框架 高朗的开发 将API设计放在第一位的生命周期. In Goa, 您的API定义不仅被描述为代码,而且还是服务器代码的来源, 客户端代码, 文档是衍生出来的. 这意味着您的代码将在API定义中使用Golang领域特定语言(DSL)进行描述。, 然后使用goa cli生成, and implemented separately from your application source code.

这就是果阿闪耀的原因. 它是一个具有良好定义的开发生命周期契约的解决方案,在生成代码时依赖于最佳实践(比如在层中划分不同的域和关注点), so transport aspects do not interfere with business aspects of the application), 遵循干净的体系结构模式,其中为传输生成可组合模块, endpoint, 以及应用程序中的业务逻辑层.

一些果阿的特征,如定义的 官方网站, include:

  • 可组合性. The package, code generation algorithms, and generated code are all modular.
  • . 传输层与实际服务实现的解耦意味着相同的服务可以公开通过多种传输(如HTTP和/或gRPC)访问的端点.
  • 关注点分离. The actual service implementation is isolated from the transport code.
  • 使用Go标准库类型. This makes it easier to interface with external code.

在本文中, 我将创建一个应用程序,并引导您完成API开发生命周期的各个阶段. 应用程序管理客户端的详细信息, 比如名字, address, 电话号码, 社交媒体, etc. 最后,我们将尝试扩展它并添加新特性来执行它的开发生命周期.

那么,我们开始吧!

准备你的发展区

Our first step is to initiate the repository and enable Go modules support:

Mkdir -p clients/design
cd clients
mod init客户端

In the end, your repo structure should be like below:

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

设计你的API

The source of truth for your API is your design definition. 如文档所述, “Goa让你独立于任何实现问题来思考你的api,然后在编写实现之前与所有利益相关者一起审查设计.” This means that every element of the API is defined here first, 在实际的应用程序代码生成之前. 说得够多了!

打开文件 客户/设计/设计.go 并添加以下内容:

/*
这是设计文件. 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.
*/
包装设计
 
import (
        	. "goa.设计/果阿/ v3 / dsl”
)
 
//主API声明
var _ = API("clients", func() {
        	标题(“客户端api”)
        	Description("This api manages clients with CRUD operations")
        	服务器("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")
        	方法("add", func() {
                    	有效载荷(func () {
                                	字段(1,"ClientID", String, "客户端ID")
                                	字段(2,"ClientName", String, "Client ID")
                                	(“ClientID”,列出)
                    	})
                    	结果(空的)
                    	错误("not_found", NotFound, "Client not found")
                    	HTTP (func () {
                                	(“/ api / v1 /客户/ {ClientID}”)
                                	响应(StatusCreated)
                    	})
        	})
 
        	方法("get", func() {
                    	有效载荷(func () {
                                	字段(1,"ClientID", String, "客户端ID")
                                	(“ClientID”)
                    	})
                    	结果(ClientManagement)
                    	错误("not_found", NotFound, "Client not found")
                    	HTTP (func () {
                                	GET (" / api / v1 /客户/ {ClientID}”)
                                	响应(StatusOK)
                    	})
        	})
        	
        	方法("show", func() {
                    	结果(CollectionOf (ClientManagement))
                    	HTTP (func () {
                                	GET (" / api / v1 /客户端”)
                                	响应(StatusOK)
                    	})
        	})
        	文件(“/ openapi.json", "./ / http / openapi世代.json")
})
 
// ClientManagement是一个自定义ResultType,用于 configure views for our custom type
var ClientManagement = ResultType("application/vnd . var "., func() {
        	Description("A ClientManagement type describes a Client of company.")
        	参考(客户端)
        	TypeName(“ClientManagement”)
 
        	属性(func () {
                    	Attribute("ClientID", String, "ID is the unique id of the Client., func() {
                                	示例(“ABCDEF12356890”)
                    	})
                    	字段(2,列出)
        	})
 
        	View("default", func() {
                    	属性(“ClientID”)
                    	属性(列出)
        	})
 
        	(“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() {
                    	示例(“ABCDEF12356890”)
        	})
        	Attribute("ClientName", String, "Name of the Client, func() {
                    	例子(“无名氏有限公司”)
        	})
        	(“ClientID”,列出)
})
 
// NotFound is a custom type where we add the queried field in the response
var NotFound = Type("NotFound", func() {)
        	说明("NotFound是" +
                    	"请求的数据不存在.")
        	Attribute("message", String, "Message of error, func() {
                    	示例("Client ABCDEF12356890 not found")
        	})
        	字段(2,"id",字符串,"缺失数据的id")
        	要求(“信息”,“id”)
})

您可以注意到的第一件事是,上面的DSL是一组Go函数,可以组合起来描述远程服务API. The functions are composed using anonymous function arguments. 在DSL函数中, we have a subset of functions that are not supposed to appear within other functions, 我们称之为顶级dsl. Below, you have a partial set of DSL的功能 and their structure:

DSL的功能

So, we have in our initial design an API top-level DSL describing our client’s API, one service top-level DSLs describing the principal API service, clients, 并提供API swagger文件, 以及两个类型顶级dsl,用于描述传输有效负载中使用的对象视图类型.

The API 函数是一个可选的顶级DSL,它列出API的全局属性,如名称, 一个描述, and also one or more servers potentially exposing different sets of services. 在我们的例子中, 一台服务器就足够了, but you could also serve different services in different tiers: 发展, test, 和生产, 例如.

The Service 函数定义了一组方法,这些方法可能映射到传输中的资源. 服务还可以定义常见的错误响应. 描述服务方法 Method. This function defines the method payload (input) and result (output) types. 如果省略有效负载或结果类型, 内置类型Empty, 在HTTP中哪一个映射到一个空的主体, is used.

最后, Type or ResultType 函数定义用户定义的类型, the main difference being that a result type also defines a set of “views.”

在我们的例子中, we described the API and explained how it should serve, 我们还创建了以下内容:

  • 一个名为 clients
  • 三种方法: add (用于创建一个客户端), get (用于检索一个客户机),以及 show (用于列出所有客户端)
  • 我们自己的定制类型, which will come in handy when we integrate with a database, 以及自定义的错误类型

Now that our application has been described, we can generate the boilerplate code. The following command takes the design package import path as an argument. It also accepts the path to the output directory as an optional flag:

Goa客户端/设计

The command outputs the names of the files it generates. 在那里, gen 目录包含存放与传输无关的服务代码的应用程序名称子目录. The http 子目录描述HTTP传输(我们有服务器和客户端代码,其中包含对请求和响应进行编码和解码的逻辑), and the CLI code to build HTTP requests from the command line). 它还包含Open API 2.JSON和YAML格式的0规范文件.

您可以复制swagger文件的内容,并将其粘贴到任何在线swagger编辑器(如在 swagger.io) for visualizing your API specification documentation. 它们同时支持YAML和JSON格式.

We are now ready for our next step in the 发展 lifecycle.

实现您的API

After your boilerplate code was created, it’s time to add some business logic to it. 在这一点上,你的代码应该是这样的:

Go中的API开发

Where every file above is maintained and updated by Goa whenever we execute the CLI. Thus, 随着体系结构的发展, 您的设计将遵循进化, 你的源代码也是如此. 要实现应用程序, 我们执行下面的命令(它将生成服务的基本实现以及可构建的服务器文件,这些文件将启动启动HTTP服务器的例程和可以向该服务器发出请求的客户端文件):

Goa客户/设计示例

This will generate a cmd folder with both server and client buildable sources. 这是你的申请, and those are the files you should maintain yourself after Goa first generates them.

Goa文档明确指出:“该命令为服务生成一个起点,以帮助引导开发-特别是当设计更改时,它不意味着重新运行.”

现在,你的代码看起来像这样:

API开发在Go: cmd文件夹

Where client.go is an example file with a dummy implementation of both get and show methods. 让我们向其中添加一些业务逻辑!

For simplicity, we will use SQLite instead of an in-memory database and Gorm as our ORM. 创建文件 sqlite.go 并添加以下内容-这将添加数据库逻辑,以在数据库上创建记录,并列出数据库中的一行和/或多行:

包的客户
import (
        	“客户/创/客户端”
        	"github.com/jinzhu/gorm”
        	_ "github.com/jinzhu/gorm/dialects/sqlite”
)
Var db *gorm.DB
Var err错误
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
函数InitDB() *gorm.DB {
        	//打开文件
        	Db, err:= gorm.Open(“sqlite3”、“./data.db")
        	//显示SQL查询
        	db.LogMode(真正的)
 
        	// Error
        	if err != nil {
                    	panic(err)
        	}
        	//创建不存在的表
        	var TableStruct = client.ClientManagement {}
        	if !db.HasTable (TableStruct) {
                    	db.不知道(TableStruct)
                    	db.集(“gorm: table_options”、“引擎= InnoDB”).不知道(TableStruct)
        	}
 
        	return db
}
 
// GetClient通过ID检索一个客户端
函数GetClient(clientID string.ClientManagement, error) {
        	db:= InitDB()
        	defer db.Close()
 
        	Var client client.ClientManagement
        	db.(“client_id = ?”,clientID).First(&clients)
        	返回客户端
}
 
// CreateClient在数据库中创建客户端行
函数CreateClient(客户端)错误{
        	db:= InitDB()
        	defer db.Close()
        	err := db.Create(&client).Error
        	return err
}
 
// ListClients retrieves the clients stored in Database
函数ListClients(.ClientManagementCollection, error) {
        	db:= InitDB()
        	defer db.Close()
        	Var client client.ClientManagementCollection
        	err := db.Find(&clients).Error
        	返回客户端
}

然后,编辑客户端.去更新客户端服务中的所有方法, implementing the database calls and constructing the API responses:

// Add实现添加.
function (s *clientsrvc.Context,
        	p *client.AddPayload) (Err错误){
        	s.logger.打印(“客户.添加了”)
        	newClient:= client.ClientManagement {
                    	ClientID: p.ClientID,
                    	列出:p.列出,
        	}
        	err = CreateClient(&newClient)
        	if err != nil {
                    	s.logger.打印("发生错误...")
                    	s.logger.Print(err)
                    	return
        	}
        	s.logger.打印(“客户.添加完成”)
        	return
}
 
// Get实现Get.
函数(s *clientsrvc)获取(ctx上下文).Context,
        	p *client.GetPayload) (res *client.ClientManagement, err错误){
        	s.logger.打印(“客户.开始”)
        	因此,犯错:= GetClient(p ..ClientID)
        	if err != nil {
                    	s.logger.打印("发生错误...")
                    	s.logger.Print(err)
                    	return
        	}
        	s.logger.打印(“客户.完成”)
        	return &因此,犯错
}
 
// Show实现Show.
function (s *clientsrvc.(res客户端.ClientManagementCollection,
        	Err错误){
        	s.logger.打印(“客户.节目开始”)
        	res, err = ListClients()
        	if err != nil {
                    	s.logger.打印("发生错误...")
                    	s.logger.Print(err)
                    	return
        	}
        	s.logger.打印(“客户.显示完成”)
        	return
}

The first cut of our application is ready to be compiled. Run the following command to create server and client binaries:

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

要运行服务器,只需运行 ./clients. 先让它开着吧. You should see it running successfully, like the following:

$ ./clients
[客户端]00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID}
[客户端]00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID}
[客户端]00:00:01 HTTP "Show" mounted on GET /api/v1/client
[客户端]00:00:01 HTTP "./ / http / openapi世代.. json”挂载在GET /openapi上.json
[clients] 00:00:01 HTTP server listening on "localhost:8080"

We are ready to perform some testing in our application. 让我们使用cli尝试所有方法:

$ ./clients-cli client add --body '{"ClientName": "Cool Company"}' \
——客户机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 has a repository 对于由社区创建的插件.

正如我之前所解释的, 作为开发生命周期的一部分, 我们可以依靠工具集通过返回设计定义来扩展我们的应用程序, 更新它, 刷新生成的代码. Let’s showcase how plugins can help by adding CORS and authentication to the API.

更新文件 客户/设计/设计.go 以下内容:

/*
这是设计文件. 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.
*/
包装设计
import (
        	. "goa.设计/果阿/ v3 / dsl”
        	cors "goa.设计/插件/ v3 /歌珥/ dsl”
)
 
//主API声明
var _ = API("clients", func() {
        	标题(“客户端api”)
        	Description("This api manages clients with CRUD operations")
        	cors.Origin("/.*localhost.*/", func() {
                    	cors.Headers("X-Authorization", "X-Time", "X-Api-Version",
                                	内容类型、来源、授权)
                    	cors.方法("GET", "POST", "OPTIONS")
                    	cors.暴露(“内容类型”,“起源”)
                    	cors.MaxAge (100)
                    	cors.凭证()
        	})
        	服务器("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 () {
                    	响应(“未经授权的”,StatusUnauthorized)
        	})
        	方法("add", func() {
                    	有效载荷(func () {
                                	TokenField(1, "token", String, func() {
                                            	描述(“用于身份验证的JWT”)
                                	})
                                	字段(2,"ClientID", String, "客户端ID")
                                	字段(3,"ClientName", String, "Client ID")
                                	字段(4,“ContactName”,字符串,“联系人名称”)
                                	字段(5,"ContactEmail",字符串,"ContactEmail")
                                	Field(6, "ContactMobile", Int, "Contact Mobile Number")
                                	(“令牌”,
                                            	"ClientID", "ClientName", "ContactName",
                                            	“ContactEmail”、“ContactMobile”)
                    	})
                    	安全(JWTAuth, func() {
                                	范围(“api:写”)
                    	})
                    	结果(空的)
                    	Error("invalid-scopes", String, "Token scopes are invalid")
                    	错误("not_found", NotFound, "Client not found")
                    	HTTP (func () {
                                	(“/ api / v1 /客户/ {ClientID}”)
                                	标题(“令牌:X-Authorization”)
                                	响应(“invalid-scopes”,StatusForbidden)
                                	响应(StatusCreated)
                    	})
        	})
 
        	方法("get", func() {
                    	有效载荷(func () {
                                	TokenField(1, "token", String, func() {
                                            	描述(“用于身份验证的JWT”)
                                	})
                                	字段(2,"ClientID", String, "客户端ID")
                    	        	(“令牌”,“ClientID”)
                    	})
                    	安全(JWTAuth, func() {
                                	范围(“api:读”)
                    	})
                    	结果(ClientManagement)
                    	Error("invalid-scopes", String, "Token scopes are invalid")
                    	错误("not_found", NotFound, "Client not found")
                    	HTTP (func () {
                                	GET (" / api / v1 /客户/ {ClientID}”)
                                	标题(“令牌:X-Authorization”)
                                	响应(“invalid-scopes”,StatusForbidden)
                                	响应(StatusOK)
                    	})
        	})
        	
        	方法("show", func() {
                    	有效载荷(func () {
                                	TokenField(1, "token", String, func() {
                                            	描述(“用于身份验证的JWT”)
                                	})
                                	(“令牌”)
                    	})
                    	安全(JWTAuth, func() {
                                	范围(“api:读”)
                    	})
                    	结果(CollectionOf (ClientManagement))
                    	Error("invalid-scopes", String, "Token scopes are invalid")
                    	HTTP (func () {
                                	GET (" / api / v1 /客户端”)
                                	标题(“令牌:X-Authorization”)
                                	响应(“invalid-scopes”,StatusForbidden)
                                	响应(StatusOK)
                    	})
        	})
        	文件(“/ openapi.json", "./ / http / openapi世代.json")
})
 
// ClientManagement是一个自定义ResultType,用于
//为我们的自定义类型配置视图
var ClientManagement = ResultType("application/vnd . var "., func() {
        	Description("A ClientManagement type describes a Client of company.")
        	参考(客户端)
        	TypeName(“ClientManagement”)
 
        	属性(func () {
                    	Attribute("ClientID", String, "ID is the unique id of the Client., func() {
                                	示例(“ABCDEF12356890”)
                    	})
                    	字段(2,列出)
                    	Attribute("ContactName", String, "Name of the Contact., func() {
                                	示例(“John Doe”)
                    	})
                    	字段(4,“ContactEmail”)
                    	“ContactMobile”字段(5日)
        	})
 
        	View("default", func() {
                    	属性(“ClientID”)
                    	属性(列出)
                    	属性(“联系名称”)
                    	属性(“ContactEmail”)
                    	属性(“ContactMobile”)
        	})
 
        	(“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() {
                    	示例(“ABCDEF12356890”)
        	})
        	Attribute("ClientName", String, "Name of the Client, func() {
                    	例子(“无名氏有限公司”)
        	})
        	Attribute("ContactName", String, "Name of the Client Contact., func() {
                    	示例(“John Doe”)
        	})
        	Attribute("ContactEmail", String, "Email of the Client Contact, func() {
                    	示例(“约翰.doe@johndoe.com")
        	})
        	Attribute("ContactMobile", Int, "Mobile number of the Client Contact, func() {
                    	例子(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() {)
        	描述("NotFound是返回的类型" +
                    	,当请求的数据不存在时.")
        	Attribute("message", String, "Message of error, func() {
                    	示例("Client ABCDEF12356890 not found")
        	})
        	字段(2,"id",字符串,"缺失数据的id")
        	要求(“信息”,“id”)
})
 
// Creds是回复令牌的自定义类型
var Creds = Type("Creds", func() {
        	字段(1,“jwt”,字符串,“jwt令牌”,func() {
                    	例子(“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
                                	“eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9”+
                                	“lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHD " +
                                	“cEfxjoYZgeFONFh7HgQ”)
        	})
        	(“jwt”)
})
 
// JWTAuth is the JWTSecurity DSL function for adding JWT support in the API
var JWTAuth = JWTSecurity("jwt", func() {
        	通过要求一个有效的
通过登录端点检索的JWT令牌. Supports
作用域"api:read"和"api:write".`)
        	作用域("api:read", " read -only access")
        	Scope("api:write", "Read and write access")
})
 
// BasicAuth是BasicAuth DSL函数
//在API中添加基本的认证支持
var BasicAuth = BasicAuthSecurity("basic", func() {
        	说明(“Basic authentication used to”+
                    	“在登录期间验证安全主体”)
        	作用域("api:read", " read -only access")
})
 
// Signin Service是用于认证用户并为他们的会话分配JWT令牌的服务
var _ = Service(“登录”,func() {
        	Description("The Signin service authenticates users and validate tokens")
        	Error("unauthorized", String, "Credentials are invalid")
        	HTTP (func () {
                    	响应(“未经授权的”,StatusUnauthorized)
        	})
        	方法("authenticate", func() {
                    	描述(“创建一个有效的JWT”)
                    	安全(BasicAuth)
                    	有效载荷(func () {
                                	Description("Credentials used to authenticate to retrieve JWT token")
                                	UsernameField(“用户名”,
                                            	字符串,"用于执行登录的用户名",func() {
                                            	示例(“用户”)
                                	})
                                	PasswordField(2,“密码”,
                                            	字符串,"用于执行登录的密码",func() {
                                            	示例(“密码”)
                                	})
                                	(“用户名”,“密码”)
                    	})
                    	结果(信誉)
                    	HTTP (func () {
                                	文章(“/ signin /验证”)
                                	响应(StatusOK)
                    	})
        	})
})

You can notice two major differences in the new design. 中定义了一个安全范围 client service so we can validate if a user is authorized to invoke the service, 我们定义了第二个服务,叫做 signin, which we will use to authenticate users and generate JSON Web Tokens (JWT), which the client 服务将用于授权调用. We have also added more fields to our custom client Type. This is a common case when developing an API—the need to reshape or restructure data.

On design, 这些改变听起来可能很简单, 但是对他们进行反思, there’s a lot of minimal features required to achieve what is described on design. Take, 例如, the architectural schematics for authentication and authorization using our API methods:

Go中的API开发: architectural schematics for authentication and authorization

Those are all new features that our code doesn’t have yet. Again, this is where Goa adds more value to your 发展 efforts. 让我们用下面的命令重新生成源代码,在传输端实现这些特性:

Goa客户端/设计

此时此刻, 如果您使用的是Git, 您将注意到新文件的存在, 与其他显示为更新. 这是因为Goa在没有我们干预的情况下无缝地相应地刷新了样板代码.

现在,我们需要实现服务端代码. 在实际应用程序中, 在更新源代码以反映所有设计更改之后,您将手动更新应用程序. 这是果阿邦建议我们采取的方式, 但是为了简单起见, I will be deleting and regenerating the example application to get us there faster. Run the commands below to delete the example application and regenerate it:

Rm -rf CMD客户端.go
Goa客户/设计示例

With that, your code should look like the following:

Go中的API开发:再生

我们可以在示例应用程序中看到一个新文件: signin.go,其中包含登录服务逻辑. 然而,我们可以看到 client.go was also updated with a JWTAuth function for validating tokens. 这符合我们在设计中所写的内容, 因此,对客户端中任何方法的每个调用都将被拦截以进行令牌验证,并且只有在得到有效令牌和正确范围的授权时才会转发.

Therefore, we will update the methods in our signin Service inside signin.go 以便添加逻辑来生成API将为经过身份验证的用户创建的令牌. 将以下上下文复制并粘贴到 signin.go:

包的客户
 
import (
        	signin“客户/创/ signin”
        	"context"
        	"log"
        	"time"
 
        	jwt”github.com/dgrijalva/jwt-go”
        	"goa.设计/果阿/ v3 /安全”
)
 
//登录服务示例实现.
// The example methods log the requests and return zero values.
类型signinsrvc struct {
        	*日志记录器.Logger
}
 
// NewSignin returns the signin service implementation.
函数NewSignin(*日志记录器 . log.Logger) signin.Service {
        	return &signinsrvc{记录器}
}
 
// BasicAuth implements the authorization logic for service "signin" for the
//“基本”安全方案.
函数(s *signinsrvc) BasicAuth(x上下文).Context,
        	User, pass字符串,scheme *security.BasicScheme)(上下文.Context,
        	error) {
 
        	if user != "gopher" && pass != "academy" {
                    	返回ctx,登录.
                                	Unauthorized("invalid username and password combination")
        	}
 
        	返回ctx, nil
}
 
//创建一个有效的JWT
函数(s *signinsrvc)验证(ctx上下文).Context,
        	p *signin.AuthenticatePayload) (res *登录.Creds,
        	Err错误){
 
        	//创建JWT令牌
        	Token:= JWT.NewWithClaims (jwt.SigningMethodHS256, jwt.MapClaims{
                    	"nbf":	time.日期(2015、10、10、12、0、0、0、时间).UTC).Unix(),
                    	"iat":	time.Now().Unix(),
                    	"exp":	time.Now().Add(time.持续时间(9)*时间.Minute).Unix(),
                    	"scopes": []string{"api:read", "api:write"},
        	})
 
        	s.logger.Printf("用户'% 5 '已登录",p.Username)
 
        	// note that if "SignedString" returns an error then it is returned as
        	//向客户端发送内部错误
        	T, err:= token.SignedString(关键)
        	if err != nil {
                    	返回nil, err
        	}
 
        	res = &signin.Creds{
                    	JWT: t,
        	}
 
        	return
}

Finally, 因为我们在自定义类型中添加了更多字段, we need to update the Add method on client Service in client.go 为了反映这些变化. 复制并粘贴以下内容以更新您的 client.go:

包的客户
 
import (
        	客户端“客户/创/客户端”
        	"context"
        	"log"
 
        	jwt”github.com/dgrijalva/jwt-go”
        	"goa.设计/果阿/ v3 /安全”
)
 
var (
        	// Key是JWT认证中使用的Key
        	Key = []byte("secret")
)
 
//客户端服务示例实现.
// The example methods log the requests and return zero values.
类型clientsrvc struct {
        	*日志记录器.Logger
}
 
// NewClient returns the client service implementation.
函数NewClient(*日志记录器 . log . log.Logger)客户端.Service {
        	return &clientsrvc{记录器}
}
 
// JWTAuth implements the authorization logic for service "client" for the
//“jwt”安全方案.
function (s *clientsrvc) JWTAuth(ctx上下文.Context,
        	令牌字符串,方案*安全性.JWTScheme)(上下文.Context,
        	error) {
        	
        	声明:= make(jwt ..MapClaims)
 
        	//授权请求
        	// 1. parse JWT token, token key is hardcoded to "secret" in this example
        	_, err:= JWT.ParseWithClaims(令牌,
                    	声明,func(_ *jwt).令牌)(接口{},
                    	错误){返回键,nil})
        	if err != nil {
                    	s.logger.Print("Unable to obtain claim from token, it's invalid")
                    	返回ctx, client.未经授权(“无效的令牌”)
        	}
 
        	s.logger.打印("检索到的声明,根据作用域进行验证")
        	s.logger.打印(索赔)
 
        	// 2. 验证提供的“作用域”声明
        	如果声明["scopes"] == nil {
                    	s.logger.Print("Unable to get scope since the scope is empty")
                    	返回ctx, client.InvalidScopes("令牌中的无效范围")
        	}
        	scope, ok:= claims[" Scopes "].({})[]接口
        	if !ok {
                    	s.logger.打印("发生错误 when retrieving the scopes")
                    	s.logger.Print(ok)
                    	返回ctx, client.InvalidScopes("令牌中的无效范围")
        	}
        	scopesInToken:= make([]string, len(scopes))
        	对于_,SCP:= range scope {
                    	scopesInToken = append(scopesInToken, scp . conf.(string))
        	}
        	如果err:= scheme.Validate(scopesInToken); err != nil {
                    	s.logger.打印("无法解析标记,检查下面的错误")
                    	返回ctx, client.InvalidScopes(犯错.Error())
        	}
        	返回ctx, nil
 
}
 
// Add实现添加.
function (s *clientsrvc.Context,
        	p *client.AddPayload) (Err错误){
        	s.logger.打印(“客户.添加了”)
        	newClient:= client.ClientManagement {
                    	ClientID: p.ClientID,
                    	列出:p.列出,
                    	联系名称:p.联系名称,
                    	ContactEmail: p.ContactEmail,
                    	ContactMobile: p.ContactMobile,
        	}
        	err = CreateClient(&newClient)
        	if err != nil {
                    	s.logger.打印("发生错误...")
                    	s.logger.Print(err)
                    	return
        	}
        	s.logger.打印(“客户.添加完成”)
        	return
}
 
// Get实现Get.
函数(s *clientsrvc)获取(ctx上下文).Context,
        	p *client.GetPayload) (res *client.ClientManagement,
        	Err错误){
        	s.logger.打印(“客户.开始”)
        	因此,犯错:= GetClient(p ..ClientID)
        	if err != nil {
                    	s.logger.打印("发生错误...")
                    	s.logger.Print(err)
                    	return
        	}
        	s.logger.打印(“客户.完成”)
        	return &因此,犯错
}
 
// Show实现Show.
function (s *clientsrvc.Context,
        	p *client.显示负载)(res客户端.ClientManagementCollection,
        	Err错误){
        	s.logger.打印(“客户.节目开始”)
        	res, err = ListClients()
        	if err != nil {
                    	s.logger.打印("发生错误...")
                    	s.logger.Print(err)
                    	return
        	}
        	s.logger.打印(“客户.显示完成”)
        	return
}

就是这样! 让我们重新编译应用程序并再次进行测试. Run the commands below to remove the old binaries and compile fresh ones:

Rm -f clients- clients-cli
go build ./ cmd /客户
go build ./ cmd / clients-cli

Run ./clients 再来一次,让它继续运行. You should see it running successfully, but this time, with the new methods implemented:

$ ./clients
[客户端]00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID}
[客户端]00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID}
[客户端]00:00:01 HTTP "Show" mounted on GET /api/v1/client
[客户端]00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client/{ClientID}
[客户端]00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client
[客户端]00:00:01 HTTP "CORS" mounted on OPTIONS /openapi.json
[客户端]00:00:01 HTTP "./ / http / openapi世代.. json”挂载在GET /openapi上.json
[客户端]00:00:01 HTTP "Authenticate" mounted on POST /signin/authenticate
[客户端]00:00:01 HTTP "CORS" mounted on OPTIONS /signin/authenticate
[clients] 00:00:01 HTTP server listening on "localhost:8080"

To test, 让我们使用cli执行所有API方法——注意,我们使用的是硬编码凭证:

$ ./client -cli signin authenticate
——用户名“gopher”——密码“academy”
{
    :“JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4 \
MSwibmJmIjoxNDQ0NDc4NDAwLCJzY29wZXMiOlsiY \
XBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\
tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw”
}
$ ./clients-cli client add——body
{"ClientName": "Cool Company", \
"ContactName": "Jane Masters", \
“ContactEmail”:“简.masters@cool.co", \
“ContactMobile”:13426547654}' \
——客户机id“1” ——令牌”eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\
joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\
tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw”
$ ./clients-cli client get——client-id "1" \
——令牌”eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\
joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\
tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw”
{
    “ClientID”:“1”,
    “客户名称”:“酷公司”,
    "ContactName": "Jane Masters",
    “ContactEmail”:“简.masters@cool.co",
    “ContactMobile”:13426547654
}
$ ./clients-cli client show
——令牌”eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\
joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\
tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw”
[
	{
        “ClientID”:“1”,
        “客户名称”:“酷公司”,
        "ContactName": "Jane Masters",
        “ContactEmail”:“简.masters@cool.co",
        “ContactMobile”:13426547654
	}
]

好了! 🎉 We have a minimalist application with proper authentication, 授权范围, 还有进化成长的空间. After this, 您可以使用云服务或您选择的任何其他身份提供者开发自己的身份验证策略. You could also create plugins for your preferred database or messaging system, 甚至可以轻松地与其他api集成.

看看果阿吧 GitHub项目 获取更多插件, examples (showing specific capabilities of the framework), 以及其他有用的资源.

今天就到这里. I hope you have enjoyed playing with Goa and reading this article. If you have any feedback about the content, feel free to reach out on GitHub, Twitter, or LinkedIn.

另外,我们在#goa频道上闲逛 打地鼠松弛所以过来打个招呼吧! 👋

了解基本知识

  • 如何创建API?

    In general, API类似于方法调用, except that its calling parameters and returns are published for broad use. It generally returns a computation or data that corresponds to its stated purpose. API也可以使用REST(表示状态传输),类似于调用Web URL.

  • 为什么API设计很重要?

    api已经成为向企业或客户提供特定应用程序特性的一种日益重要的方式. 例如, third-party sellers on Amazon use the company’s APIs to gain access to the platform, enabling them to have a much broader reach than they would otherwise.

  • 2020年的Golang值得学习吗?

    Golang is frequently used as a fast way of prototyping new concepts or writing scripts. 它是一种非常容易学习的语言,可以被开发人员用作学习其他语言的垫脚石.

  • Golang有未来吗?

    虽然不一定是主流语言, Golang has a smaller but vocal and active user community, which guarantees a significant role for the language in the years ahead.

就这一主题咨询作者或专家.
预约电话
格莱德森·纳西门托的头像
Gleidson Nascimento

Located in 惠灵顿,新西兰

成员自 2019年1月10日

作者简介

Gleidson是一位经验丰富的工程师,拥有基础设施自动化架构方面的技能, design, 发展, 和编制.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

以前在

IBM

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® community.