作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
格莱德森·纳西门托的头像

Gleidson Nascimento

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

Expertise

以前在

IBM
Share

API开发是当今的一个热门话题. 有很多方法可以开发和交付API, 大公司已经开发了大量的解决方案来帮助您快速启动应用程序.

然而,这些选项中的大多数都缺少一个关键特性:开发生命周期管理. So, 开发人员花了一些周期来创建有用且健壮的API,但最终却要与预期的代码有机演变以及API在源代码中的微小变化所带来的影响作斗争.

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

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

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

  • 可组合性. 包、代码生成算法和生成的代码都是模块化的.
  • . 传输层与实际服务实现的解耦意味着相同的服务可以公开通过多种传输(如HTTP和/或gRPC)访问的端点.
  • 关注点分离. 实际的服务实现与传输代码是隔离的.
  • 使用Go标准库类型. 这使得它更容易与外部代码进行交互.

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

那么,我们开始吧!

准备你的发展区

我们的第一步是启动存储库并启用Go模块支持:

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

最后,你的回购结构应该是这样的:

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

设计你的API

API的真实来源是您的设计定义. 如文档所述, “Goa让你独立于任何实现问题来思考你的api,然后在编写实现之前与所有利益相关者一起审查设计.这意味着API的每个元素都首先在这里定义, 在实际的应用程序代码生成之前. 说得够多了!

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

/*
这是设计文件. 它包含API规范、方法、输入和使用Goa DSL代码的输出. 我们的目标是将其用作整个API源代码的单一事实来源.
*/
包装设计
 
import (
        	. "goa.设计/果阿/ v3 / dsl”
)
 
//主API声明
var _ = API("clients", func() {
        	标题(“客户端api”)
        	描述(“这个api用CRUD操作管理客户端”)
        	服务器("clients", func() {
                    	Host("localhost", func() {
                                	URI (http://localhost: 8080 / api / v1)
                    	})
        	})
})
 
//使用两个方法的客户端服务声明和Swagger API规范文件
var _ = Service("client", func() {
        	描述(“客户端服务允许访问客户端成员”)
        	方法("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,用于为自定义类型配置视图
var ClientManagement = ResultType("application/vnd . var "., func() {
        	描述(“ClientManagement类型描述了公司的客户端.")
        	参考(客户端)
        	TypeName(“ClientManagement”)
 
        	属性(func () {
                    	Attribute("ClientID", String, "ID是客户端的唯一ID。., func() {
                                	示例(“ABCDEF12356890”)
                    	})
                    	字段(2,列出)
        	})
 
        	View("default", func() {
                    	属性(“ClientID”)
                    	属性(列出)
        	})
 
        	(“ClientID”)
})
 
//客户端是数据库中客户端的自定义类型
var Client = Type("Client", func() {
        	描述(“客户描述公司的客户.")
        	Attribute("ClientID", String, "ID是Client Member的唯一ID。., func() {
                    	示例(“ABCDEF12356890”)
        	})
        	属性("ClientName",字符串,"客户名",func() {
                    	例子(“无名氏有限公司”)
        	})
        	(“ClientID”,列出)
})
 
// NotFound是一个自定义类型,我们在响应中添加查询字段
var NotFound = Type("NotFound", func() {)
        	说明("NotFound是" +
                    	"请求的数据不存在.")
        	属性("message", String, "message of error", func() {
                    	示例("Client ABCDEF12356890 not found")
        	})
        	字段(2,"id",字符串,"缺失数据的id")
        	要求(“信息”,“id”)
})

您可以注意到的第一件事是,上面的DSL是一组Go函数,可以组合起来描述远程服务API. 这些函数是使用匿名函数参数组成的. 在DSL函数中, 我们有一个函数子集,它不应该出现在其他函数中, 我们称之为顶级dsl. 下面是DSL函数及其结构的部分集合:

DSL的功能

So, 在最初的设计中,我们有一个API顶级DSL来描述客户端的API, 一个描述主要API服务的服务顶级dsl, clients, 并提供API swagger文件, 以及两个类型顶级dsl,用于描述传输有效负载中使用的对象视图类型.

The API 函数是一个可选的顶级DSL,它列出API的全局属性,如名称, 一个描述, 还有一个或多个服务器可能公开不同的服务集. 在我们的例子中, 一台服务器就足够了, 但是你也可以在不同的层次提供不同的服务:开发, test, 和生产, 例如.

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

最后, Type or ResultType 函数定义用户定义的类型, 主要区别在于结果类型还定义了一组“视图”.”

在我们的例子中, 我们描述了API并解释了它应该如何服务, 我们还创建了以下内容:

  • 一个名为 clients
  • 三种方法: add (用于创建一个客户端), get (用于检索一个客户机),以及 show (用于列出所有客户端)
  • 我们自己的定制类型, 当我们与数据库集成时,哪个会派上用场, 以及自定义的错误类型

现在已经描述了我们的应用程序,我们可以生成样板代码了. 下面的命令将设计包导入路径作为参数. 它还接受输出目录的路径作为可选标志:

Goa客户端/设计

该命令输出它生成的文件的名称. 在那里, gen 目录包含存放与传输无关的服务代码的应用程序名称子目录. The http 子目录描述HTTP传输(我们有服务器和客户端代码,其中包含对请求和响应进行编码和解码的逻辑), 以及从命令行构建HTTP请求的CLI代码). 它还包含Open API 2.JSON和YAML格式的0规范文件.

您可以复制swagger文件的内容,并将其粘贴到任何在线swagger编辑器(如在 swagger.io)来可视化您的API规范文档. 它们同时支持YAML和JSON格式.

现在我们已经为开发生命周期的下一步做好了准备.

实现您的API

在创建样板代码之后,是时候向其添加一些业务逻辑了. 在这一点上,你的代码应该是这样的:

Go中的API开发

每当我们执行CLI时,上面的每个文件都由Goa维护和更新. Thus, 随着体系结构的发展, 您的设计将遵循进化, 你的源代码也是如此. 要实现应用程序, 我们执行下面的命令(它将生成服务的基本实现以及可构建的服务器文件,这些文件将启动启动HTTP服务器的例程和可以向该服务器发出请求的客户端文件):

Goa客户/设计示例

这将生成一个cmd文件夹,其中包含服务器和客户端可构建的源代码. 这是你的申请, 这些文件是果阿第一次生成后你应该自己维护的.

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

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

API开发在Go: cmd文件夹

Where client.go 一个示例文件是否具有两者的虚拟实现 get and show methods. 让我们向其中添加一些业务逻辑!

为简单起见,我们将使用SQLite而不是内存数据库,并使用Gorm作为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是启动数据库文件和表结构的函数
//如果未创建,则返回用于next函数的db对象
函数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检索存储在Database中的客户端
函数ListClients(.ClientManagementCollection, error) {
        	db:= InitDB()
        	defer db.Close()
        	Var client client.ClientManagementCollection
        	err := db.Find(&clients).Error
        	返回客户端
}

然后,编辑客户端.去更新客户端服务中的所有方法, 实现数据库调用和构造API响应;

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

我们的应用程序的第一部分已经准备好编译了. 运行如下命令创建服务端和客户端二进制文件:

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

要运行服务器,只需运行 ./clients. 先让它开着吧. 您应该看到它成功运行,如下所示:

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

我们准备在我们的应用程序中执行一些测试. 让我们使用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 对于由社区创建的插件.

正如我之前所解释的, 作为开发生命周期的一部分, 我们可以依靠工具集通过返回设计定义来扩展我们的应用程序, 更新它, 刷新生成的代码. 让我们通过向API添加CORS和身份验证来展示插件如何提供帮助.

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

/*
这是设计文件. 它包含API规范、方法、输入和使用Goa DSL代码的输出. 我们的目标是将其用作整个API源代码的单一事实来源.
*/
包装设计
import (
        	. "goa.设计/果阿/ v3 / dsl”
        	cors "goa.设计/插件/ v3 /歌珥/ dsl”
)
 
//主API声明
var _ = API("clients", func() {
        	标题(“客户端api”)
        	描述(“这个api用CRUD操作管理客户端”)
        	cors.Origin("/.*localhost.*/", func() {
                    	cors.报头("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)
                    	})
        	})
})
 
//使用两个方法的客户端服务声明和Swagger API规范文件
var _ = Service("client", func() {
        	描述(“客户端服务允许访问客户端成员”)
        	错误("未经授权",字符串,"凭据无效")
        	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")
                                	字段(6,“ContactMobile”,Int,“联系人手机号码”)
                                	(“令牌”,
                                            	"ClientID", "ClientName", "ContactName",
                                            	“ContactEmail”、“ContactMobile”)
                    	})
                    	安全(JWTAuth, func() {
                                	范围(“api:写”)
                    	})
                    	结果(空的)
                    	错误("invalid-scopes",字符串,"令牌作用域无效")
                    	错误("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)
                    	错误("invalid-scopes",字符串,"令牌作用域无效")
                    	错误("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))
                    	错误("invalid-scopes",字符串,"令牌作用域无效")
                    	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() {
        	描述(“ClientManagement类型描述了公司的客户端.")
        	参考(客户端)
        	TypeName(“ClientManagement”)
 
        	属性(func () {
                    	Attribute("ClientID", String, "ID是客户端的唯一ID。., func() {
                                	示例(“ABCDEF12356890”)
                    	})
                    	字段(2,列出)
                    	属性(“ContactName”,字符串,“联系人的名称”)., func() {
                                	示例(“John Doe”)
                    	})
                    	字段(4,“ContactEmail”)
                    	“ContactMobile”字段(5日)
        	})
 
        	View("default", func() {
                    	属性(“ClientID”)
                    	属性(列出)
                    	属性(“联系名称”)
                    	属性(“ContactEmail”)
                    	属性(“ContactMobile”)
        	})
 
        	(“ClientID”)
})
 
//客户端是数据库中客户端的自定义类型
var Client = Type("Client", func() {
        	描述(“客户描述公司的客户.")
        	Attribute("ClientID", String, "ID是Client Member的唯一ID。., func() {
                    	示例(“ABCDEF12356890”)
        	})
        	属性("ClientName",字符串,"客户名",func() {
                    	例子(“无名氏有限公司”)
        	})
        	属性("ContactName", String, "客户联系人的名称")., func() {
                    	示例(“John Doe”)
        	})
        	属性("ContactEmail",字符串,"客户联系人的电子邮件",函数(){
                    	示例(“约翰.doe@johndoe.com")
        	})
        	属性("ContactMobile", Int, "客户端联系人的手机号码",func() {
                    	例子(12365474235)
        	})
        	Required("ClientID", "ClientName", "ContactName", "ContactEmail", "ContactMobile")
})
 
// NotFound是一个自定义类型,我们在响应中添加查询字段
var NotFound = Type("NotFound", func() {)
        	描述("NotFound是返回的类型" +
                    	,当请求的数据不存在时.")
        	属性("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是JWTSecurity DSL函数,用于在API中添加JWT支持
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() {
        	描述(“Signin服务对用户进行身份验证并验证令牌”)
        	错误("未经授权",字符串,"凭据无效")
        	HTTP (func () {
                    	响应(“未经授权的”,StatusUnauthorized)
        	})
        	方法("authenticate", func() {
                    	描述(“创建一个有效的JWT”)
                    	安全(BasicAuth)
                    	有效载荷(func () {
                                	描述(“用于身份验证以检索JWT令牌的凭据”)
                                	UsernameField(“用户名”,
                                            	字符串,"用于执行登录的用户名",func() {
                                            	示例(“用户”)
                                	})
                                	PasswordField(2,“密码”,
                                            	字符串,"用于执行登录的密码",func() {
                                            	示例(“密码”)
                                	})
                                	(“用户名”,“密码”)
                    	})
                    	结果(信誉)
                    	HTTP (func () {
                                	文章(“/ signin /验证”)
                                	响应(StatusOK)
                    	})
        	})
})

您可以注意到新设计中的两个主要区别. 中定义了一个安全范围 client 服务,以便我们可以验证用户是否被授权调用该服务, 我们定义了第二个服务,叫做 signin,我们将使用它来验证用户并生成JSON Web令牌(JWT) client 服务将用于授权调用. 我们还向自定义客户端Type添加了更多字段. 在开发api时,这是一种常见的情况——需要重塑或重构数据.

On design, 这些改变听起来可能很简单, 但是对他们进行反思, 要实现在设计中描述的内容,需要许多最小的功能. Take, 例如, 使用我们的API方法进行身份验证和授权的架构示意图:

Go中的API开发:身份验证和授权的架构原理图

这些都是我们的代码还没有的新特性. 同样,这也是Goa为您的开发工作增加更多价值的地方. 让我们用下面的命令重新生成源代码,在传输端实现这些特性:

Goa客户端/设计

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

现在,我们需要实现服务端代码. 在实际应用程序中, 在更新源代码以反映所有设计更改之后,您将手动更新应用程序. 这是果阿邦建议我们采取的方式, 但是为了简单起见, 我将删除并重新生成示例应用程序,以使我们更快地到达那里. 运行以下命令删除示例应用程序并重新生成它:

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

这样,你的代码应该如下所示:

Go中的API开发:再生

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

因此,我们将在内部更新我们的signin Service中的方法 signin.go 以便添加逻辑来生成API将为经过身份验证的用户创建的令牌. 将以下上下文复制并粘贴到 signin.go:

包的客户
 
import (
        	signin“客户/创/ signin”
        	"context"
        	"log"
        	"time"
 
        	jwt”github.com/dgrijalva/jwt-go”
        	"goa.设计/果阿/ v3 /安全”
)
 
//登录服务示例实现.
//示例方法记录请求并返回零值.
类型signinsrvc struct {
        	*日志记录器.Logger
}
 
// NewSignin返回登录服务实现.
函数NewSignin(*日志记录器 . log.Logger) signin.Service {
        	return &signinsrvc{记录器}
}
 
// BasicAuth实现服务“signin”的授权逻辑
//“基本”安全方案.
函数(s *signinsrvc) BasicAuth(x上下文).Context,
        	User, pass字符串,scheme *security.BasicScheme)(上下文.Context,
        	error) {
 
        	if user !=“地鼠” && pass != "academy" {
                    	返回ctx,登录.
                                	未经授权(“无效的用户名和密码组合”)
        	}
 
        	返回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)
 
        	//注意,如果"SignedString"返回错误,则返回为
        	//向客户端发送内部错误
        	T, err:= token.SignedString(关键)
        	if err != nil {
                    	返回nil, err
        	}
 
        	res = &signin.Creds{
                    	JWT: t,
        	}
 
        	return
}

Finally, 因为我们在自定义类型中添加了更多字段, 我们需要更新客户端服务中的Add方法 client.go 为了反映这些变化. 复制并粘贴以下内容以更新您的 client.go:

包的客户
 
import (
        	客户端“客户/创/客户端”
        	"context"
        	"log"
 
        	jwt”github.com/dgrijalva/jwt-go”
        	"goa.设计/果阿/ v3 /安全”
)
 
var (
        	// Key是JWT认证中使用的Key
        	Key = []byte("secret")
)
 
//客户端服务示例实现.
//示例方法记录请求并返回零值.
类型clientsrvc struct {
        	*日志记录器.Logger
}
 
// NewClient返回客户端服务实现.
函数NewClient(*日志记录器 . log . log.Logger)客户端.Service {
        	return &clientsrvc{记录器}
}
 
// JWTAuth实现服务"client"的授权逻辑
//“jwt”安全方案.
function (s *clientsrvc) JWTAuth(ctx上下文.Context,
        	令牌字符串,方案*安全性.JWTScheme)(上下文.Context,
        	error) {
        	
        	声明:= make(jwt ..MapClaims)
 
        	//授权请求
        	// 1. 解析JWT令牌,令牌密钥在本例中硬编码为“secret”
        	_, err:= JWT.ParseWithClaims(令牌,
                    	声明,func(_ *jwt).令牌)(接口{},
                    	错误){返回键,nil})
        	if err != nil {
                    	s.logger.打印(“无法从令牌获取声明,它无效”)
                    	返回ctx, client.未经授权(“无效的令牌”)
        	}
 
        	s.logger.打印("检索到的声明,根据作用域进行验证")
        	s.logger.打印(索赔)
 
        	// 2. 验证提供的“作用域”声明
        	如果声明["scopes"] == nil {
                    	s.logger.打印("无法获取作用域,因为作用域为空")
                    	返回ctx, client.InvalidScopes("令牌中的无效范围")
        	}
        	scope, ok:= claims[" Scopes "].({})[]接口
        	if !ok {
                    	s.logger.打印(“检索作用域时发生错误”)
                    	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
}

就是这样! 让我们重新编译应用程序并再次进行测试. 运行以下命令删除旧的二进制文件并编译新的:

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

Run ./clients 再来一次,让它继续运行. 您应该会看到它成功运行,但这一次,实现了新的方法:

$ ./clients
[客户端]00:00:01 HTTP "Add"挂载在POST /api/v1/client/{ClientID}
[client] 00:00:01 HTTP "Get"挂载在Get /api/v1/client/{ClientID}
[客户端]00:00:01 HTTP "Show"挂载在GET /api/v1/client上
[客户端]00:00:01 HTTP "CORS"挂载在OPTIONS /api/v1/client/{ClientID}
[client] 00:00:01 HTTP "CORS"挂载在OPTIONS /api/v1/client上
[client] 00:00:01 HTTP "CORS"挂载在OPTIONS /openapi上.json
[客户端]00:00:01 HTTP "./ / http / openapi世代.. json”挂载在GET /openapi上.json
[client] 00:00:01 HTTP "Authenticate"挂载在POST /signin/ Authenticate上
[client] 00:00:01 HTTP "CORS"挂载在OPTIONS /signin/authenticate上
[客户端]00:00:01 HTTP服务器监听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}' \
——client-id "1"——token "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
	}
]

好了! 🎉我们有一个具有适当身份验证的极简应用程序, 授权范围, 还有进化成长的空间. After this, 您可以使用云服务或您选择的任何其他身份提供者开发自己的身份验证策略. 您还可以为您的首选数据库或消息传递系统创建插件, 甚至可以轻松地与其他api集成.

看看果阿吧 GitHub项目 获取更多插件, 示例(显示框架的特定功能), 以及其他有用的资源.

今天就到这里. 我希望你喜欢在果阿打球,也喜欢读这篇文章. 如果您对内容有任何反馈,请随时与我们联系 GitHub, Twitter, or LinkedIn.

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

了解基本知识

  • 如何创建API?

    In general, API类似于方法调用, 除了它的调用参数和返回值被发布以供广泛使用之外. 它通常返回符合其声明目的的计算或数据. API也可以使用REST(表示状态传输),类似于调用Web URL.

  • 为什么API设计很重要?

    api已经成为向企业或客户提供特定应用程序特性的一种日益重要的方式. 例如, 亚马逊上的第三方卖家使用该公司的api来访问该平台, 使他们能够拥有比其他方式更广泛的影响力.

  • 2020年的Golang值得学习吗?

    Golang经常被用作创建新概念原型或编写脚本的快速方法. 它是一种非常容易学习的语言,可以被开发人员用作学习其他语言的垫脚石.

  • Golang有未来吗?

    虽然不一定是主流语言, Golang有一个较小但活跃的用户社区, 这就保证了英语在未来的岁月里会扮演重要的角色.

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

Located in 惠灵顿,新西兰

成员自 2019年1月10日

作者简介

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

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

以前在

IBM

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

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

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

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

Toptal开发者

加入总冠军® community.