作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
编者注:本文由我们的编辑团队于2022年12月2日更新. 它已被修改,以包括最近的来源,并与我们目前的编辑标准保持一致.
应用程序编程接口(api)无处不在. 它们使软件能够始终如一地与软件的其他部分(内部或外部)进行通信, 可扩展性的关键因素是什么, 更不用说可重用性了.
如今,在线服务拥有面向公众的api非常普遍. 这使得其他开发人员可以轻松地集成社交媒体登录等功能, 信用卡付款, 行为跟踪. The de facto 他们为此使用的标准称为具象状态转移(REST).
And why build a Node.特别是REST API? 虽然许多平台和编程语言可以用于类任务 ASP.NET Core, Laravel (PHP), or 瓶(Python)-JavaScript仍然是 最流行的语言 专业开发人员. 因此,在本教程中,我们的基本但安全的REST API后端将侧重于常见的组件 JavaScript开发人员:
学习本教程的开发人员还应该熟悉终端(或命令提示符)。.
注意:我们不会在这里讨论前端代码库, 但事实上,我们的后端是用JavaScript编写的,这使得共享代码对象模型变得很方便, 例如,在整个堆栈中.
REST api用于使用一组通用的无状态操作来访问和操作数据. 这些操作是HTTP协议的组成部分,代表了基本的创建, read, update, 和删除(CRUD)功能, 一对一的:虽然不是一对一的方式:
POST
(创建资源或提供数据)GET
(检索资源索引或单个资源)PUT
(创建或替换资源)PATCH
(更新/修改资源)DELETE
(删除资源)使用这些HTTP操作和一个资源名作为地址,我们可以构建一个Node.通过为每个操作创建一个端点来使用REST API. 通过实现模式, 我们将拥有一个稳定且易于理解的基础,使我们能够快速地开发代码并在之后维护它. 同样的基础将用于集成第三方功能, 其中大多数同样使用REST api, 使这种集成更快.
现在,让我们开始创建安全节点.REST API.
在本教程中, 我们将为资源调用创建一个非常通用(并且非常实用)的安全REST API users
.
我们的资源将具有以下基本结构:
id
(自动生成的UUID)firstName
lastName
email
password
permissionLevel
(允许这个用户做什么?)我们将为该资源创建以下操作:
POST
在端点上 /users
(创建新用户)GET
在端点上 /users
(列出所有用户)GET
在端点上 /用户/:userId
(获取特定用户)PATCH
在端点上 /用户/:userId
(更新特定用户的数据)DELETE
在端点上 /用户/:userId
(删除特定用户)我们还将使用JSON web令牌(jwt)作为访问令牌. 为此,我们将创建另一个名为 auth
这将需要用户的电子邮件和密码, in return, 会在某些操作上生成用于身份验证的令牌吗. Dejan Milosevic的一篇很棒的文章 用于Java中安全REST应用程序的JWT goes into further detail about this; the principles are the same.)
首先,确保您拥有最新的Node.已安装的Js版本. 对于本文,我将使用版本14.9.0; it may also work on older versions.
接下来,确保你有 MongoDB 安装. 我们不会解释这里使用的Mongoose和MongoDB的细节, 但是要让基本的东西运行起来, 只需以交互模式启动服务器(例如.e.,从命令行输入 mongo
),而不是作为一种服务. 这是因为, 在本教程的某一点上, 我们需要直接与MongoDB交互,而不是通过我们的Node.js code.
注意:使用MongoDB, 不需要像在某些RDBMS场景中那样创建特定的数据库. 来自Node的第一个插入调用.Js代码会自动触发它的创建.
本教程不包含工作项目所需的所有代码. 而是让你克隆 配套回购 当你通读时,只要跟着要点走就行了. 但是,如果您愿意,也可以根据需要从repo中复制特定的文件和片段.
导航到结果 rest-api-tutorial /
在终端中的文件夹. 你会看到我们的项目包含三个模块文件夹:
common
(处理所有共享服务,以及用户模块之间共享的信息)users
(一切与用户有关)auth
(处理JWT生成和登录流)Now, run npm安装
(or yarn
如果你有的话).
祝贺你! 现在,您已经拥有了运行简单Node所需的所有依赖项和设置.. REST API后端.
我们将使用 Mongoose,物体 数据建模 (ODM)库,用于在用户模式中创建用户模型.
首先,我们需要在中创建Mongoose模式 /用户/模型/用户.model.js
:
const userSchema = new Schema({
firstName:字符串,
姓:字符串,
电子邮件:字符串,
密码:字符串,
permissionLevel:数量
});
一旦定义了模式,就可以轻松地将模式附加到用户模型上.
const userModel =猫鼬.模型(“用户”,userSchema);
在那之后, 我们可以使用这个模型来实现Express中需要的所有CRUD操作.js端点.
让我们从定义Express的“创建用户”操作开始.j的路由 用户/路线.config.js
:
app.邮报》(' /用户的,
UsersController.insert
]);
这是我们的快车.Js应用程序在主 index.js
file. The UsersController
对象从控制器导入,在控制器中对密码进行适当的散列,定义为 /用户/控制器/用户.控制器.js
:
exports.insert = (req, res) => {
设salt = crypto.randomBytes (16).toString(“base64”);
让hash = crypto.createHmac(“sha512”、盐)
.更新(要求.body.password)
.消化(“base64”);
req.body.Password = salt + "$" + hash;
req.body.permissionLevel = 1;
UserModel.createUser(要求.body)
.then((result) => {
res.状态(201).发送({id:结果._id});
});
};
此时,我们可以通过运行Node来测试Mongoose模型.. js API服务器(npm开始
),并发送 POST
请求 /users
一些JSON数据:
{
“firstName”:“Marcos”,
"lastName": "Silva",
"电邮":"马科斯.henrique@ruansaen.com",
"password": " s3cr3tp4ssw4rd "
}
有几个工具可以用于此. 我们将在下面介绍失眠,但你也可以使用 Postman 或者像cURL(一个命令行工具)这样的开源替代品 Bruno. 例如,您甚至可以只使用javascript, 从浏览器的内置开发工具控制台中:
fetch (http://localhost: 3600 /用户,{
方法:“文章”,
标题:{
“内容类型”:“application / json”
},
身体:JSON.stringify ({
“firstName”:“马科斯”,
“姓”:“席尔瓦”,
“电子邮件”:“马科斯.henrique@ruansaen.com",
“密码”:“s3cr3tp4sswo4rd”
})
})
.然后(函数(响应){
返回响应.json();
})
.然后(函数(数据){
console.log('请求成功,JSON响应',数据);
})
.抓住(函数(错误){
console.log('请求失败',错误);
});
在这一点上,一个有效的帖子的结果将只是来自创建的用户的ID: {“id”:“5 b02c5c84817bf28049e58a3”}
. 我们还需要加上 createUser
方法导入模型 用户/模型/用户.model.js
:
exports.createUser = (userData) => {
const user = new user (userData);
返回用户.save();
};
现在我们需要查看用户是否存在. 为此,我们将为的实现“获取用户id”特性 用户/:用户标识
endpoint.
首先,我们创建一个Express.j的路由 /用户/线路/配置.js
:
app.get(/用户/:userId, (
UsersController.getById
]);
然后,在中创建控制器 /用户/控制器/用户.控制器.js
:
exports.getById = (req, res) => {
UserModel.findById(要求.params.userId).then((result) => {
res.状态(200).发送(结果);
});
};
最后,加入 findById
方法导入模型 /用户/模型/用户.model.js
:
exports.findById = (id) => {
返回用户.findById (id).then((result) => {
结果=结果.toJSON();
删除的结果._id;
删除的结果.__v;
返回结果;
});
};
响应看起来像这样:
{
“firstName”:“马科斯”,
“姓”:“席尔瓦”,
“电子邮件”:“马科斯.henrique@ruansaen.com",
“密码”:“Y + XZEaR7J8xAQCc37nf1rw = = $ p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh + CUQ4n / E0z48mp8SDTpX2ivuQ = = ",
“permissionLevel”:1、
“id”:“5 b02c5c84817bf28049e58a3”
}
注意,我们可以看到散列密码. 对于本教程, 我们正在显示密码, 但最好的做法是永远不要泄露密码, 即使它已经被散列了. 我们还可以看到 permissionLevel
,稍后我们将使用它来处理用户权限.
重复上述模式,我们现在可以添加更新用户的功能. 我们将使用 PATCH
操作,因为它将使我们能够只发送我们想要更改的字段. 的表达.因此,他的路线将是 PATCH
to /用户/:userid
,我们将发送任何我们想要更改的字段. 我们还需要实现一些额外的验证,因为更改应该仅限于有问题的用户或管理员, 并且只有管理员应该能够更改 permissionLevel
. 我们现在将跳过它,并在实现auth模块后回到它. 现在,我们的控制器看起来像这样:
exports.patchById = (req, res) => {
if (req.body.密码){
设salt = crypto.randomBytes (16).toString(“base64”);
让hash = crypto.createHmac(“sha512”、盐).更新(要求.body.password).消化(“base64”);
req.body.Password = salt + "$" + hash;
}
UserModel.patchUser(要求.params.userId,要求.body).then((result) => {
res.状态(204).send({});
});
};
默认情况下, 我们将发送一个没有响应体的HTTP代码204,以表明请求成功.
我们需要加上 patchUser
模型方法:
exports.patchUser = (id, userData) => {
返回用户.findOneAndUpdate ({
_id: id
}、用户数据);
};
下面的控制器将把用户列表实现为 GET
at /users/
:
exports.list = (req, res) => {
设limit = req.query.limit && req.query.limit <= 100 ? 方法(申请.query.(上限):10;
让page = 0;
if (req.query) {
if (req.query.page) {
req.query.page = parseInt.query.page);
页码=号码.isInteger(要求.query.page) ? req.query.page : 0;
}
}
UserModel.列表(极限,页面).then((result) => {
res.状态(200).发送(结果);
})
};
相应的模型方法为:
exports.list = (perPage, page) => {
return new Promise((resolve, reject) => {
User.find()
.限制(perPage)
.跳过(perPage * page)
.执行(function (err, users) {
If (err) {
拒绝(错);
} else {
解决(用户);
}
})
});
};
生成的列表响应将具有以下结构:
[
{
“firstName”:“马可”,
“姓”:“席尔瓦”,
“电子邮件”:“马科斯.henrique@ruansaen.com",
“密码”:“z4tS / DtiH + 0 gb4j6qn1k3w = = $ al6sGxKBKqxRQkDmhnhQpEB6 + DQgDRH2qr47BZcqLm4 / fphZ7 + a9U + HhxsNaSnGB2l05Oem / BLIOkbtOuw1tXA = = ",
“permissionLevel”:1、
“id”:“5 b02c5c84817bf28049e58a3”
},
{
“firstName”:“保罗”,
“姓”:“席尔瓦”,
“电子邮件”:“马科斯.henrique2@ruansaen.com",
“密码”:“wTsqO1kHuVisfDIcgl5YmQ = = $ cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw = = ",
“permissionLevel”:1、
“id”:“5 b02d038b653603d1ca69729”
}
]
最后要实现的部分是 DELETE
at /用户/:userId
.
我们的删除控制器将是:
exports.removeById = (req, res) => {
UserModel.removeById(要求.params.userId)
.then((result)=>{
res.状态(204).send({});
});
};
与之前一样,控制器将返回HTTP代码204,没有内容体作为确认.
相应的模型方法应该是这样的:
exports.removeById = (userId) => {
return new Promise((resolve, reject) => {
User.deleteMany({_id: userId}, (err) => {
If (err) {
拒绝(错);
} else {
解决(err);
}
});
});
};
现在我们有了操作用户资源所需的所有操作, 我们完成了用户控制器. 这段代码的主要思想是向您提供使用REST模式的核心概念. 我们需要返回到这段代码来实现对它的一些验证和权限, 但首先我们需要开始建立我们的安全. 让我们创建auth模块.
在我们确保 users
模块通过实现权限和验证中间件, 我们需要能够为当前用户生成有效的令牌. 我们将生成一个JWT,以响应提供有效电子邮件和密码的用户. JWT允许用户安全地发出多个请求,而无需重复验证. 它通常有一个有效期, 为了保证通信安全,每隔几分钟就会重新创建一个新的令牌. 对于本教程, though, 我们将放弃刷新令牌,并保持每次登录单个令牌的简单性.
首先,我们将为 POST
请求 /auth
resource. 请求正文将包含用户的电子邮件和密码:
{
"电邮":"马科斯.henrique2@ruansaen.com",
password: " s3cr3tp4ssw4rd2 "
}
在使用控制器之前,我们应该验证用户 /授权/中间件)/验证.user.中间件.js
:
exports.isPasswordAndUserMatch = (req, res, next) => {
UserModel.findByEmail(要求.body.email)
.then((user)=>{
if(!user[0]){
res.状态(404).send({});
}else{
let passwordFields = user[0].password.分割(美元);
let salt = passwordFields[0];
让hash = crypto.createHmac(“sha512”、盐)
.更新(要求.body.password)
.消化(“base64”);
if (hash === passwordFields[1]) {
req.body = {
用户名:用户[0]._id,
电子邮件:用户[0].email,
permissionLevel:用户[0].permissionLevel,
提供者:“电子邮件”,
名称:用户[0].firstName + ' ' + user[0].lastName,
};
返回下一个();
} else {
返回res.状态(400).send({errors:['无效的电子邮件或密码']});
}
}
});
};
完成这些后,我们可以转向控制器并生成JWT:
exports.login = (req, res) => {
try {
let refreshId = req.body.userId + jwtSecret;
设salt = crypto.randomBytes (16).toString(“base64”);
让hash = crypto.createHmac(“sha512”、盐).更新(refreshId).消化(“base64”);
req.body.refreshKey = salt;
让token = JWT.sign(req.身体,jwtSecret);
让b = Buffer.从(散列);
让refresh_token = b.toString(“base64”);
res.状态(201).send({accessToken: token, refreshToken: refresh_token});
} catch (err) {
res.状态(500).发送({错误:错误});
}
};
尽管在本教程中我们不会刷新令牌, 控制器的设置是为了使这种生成更容易在随后的开发中实现.
我们现在要做的就是创造快车.. Js路由并调用适当的中间件 /授权/路线.config.js
:
app.文章(“/认证”,
VerifyUserMiddleware.hasAuthValidFields,
VerifyUserMiddleware.isPasswordAndUserMatch,
授权Controller.login
]);
响应将在accessToken字段中包含生成的JWT:
{
:“accessToken eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmng i44vqluewp3yiayxvo - 74803 - v1mu y9qpuq5vy”,
:“refreshToken U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ = = "
}
创建令牌之后,我们可以在 授权
头文件使用表单 不记名ACCESS_TOKEN
.
我们首先要确定的是谁可以使用 users
resource. 以下是我们需要处理的场景:
确定了这些场景之后, 我们首先需要一个中间件,它总是验证用户是否使用了有效的JWT. 中间件 /共同/中间件)/身份验证.验证.中间件.js
可以这么简单:
exports.validJWTNeeded = (req, res, next) => {
if (req.标题(“授权”)){
try {
让authorization = req.标题(“授权”).分割(' ');
如果(授权[0] !== '承载者'){
返回res.状态(401).send();
} else {
req.JWT = JWT.验证(授权[1],秘密);
返回下一个();
}
} catch (err) {
返回res.状态(403).send();
}
} else {
返回res.状态(401).send();
}
};
我们将使用HTTP错误码来处理请求错误:
我们可以使用位与运算符(位掩码)来控制权限. 如果我们将每个需要的权限设置为2的幂, 我们可以将32位整数的每一位视为单个权限. 通过将权限值设置为2147483647,管理员可以拥有所有权限. 然后,该用户可以访问任何路由. 另一个例子, 权限值设置为7的用户将对值为1的位标记的角色具有权限, 2, 4(2的0次方, 1, and 2).
中间件看起来像这样:
exports.minimumPermissionLevelRequired = (required_permission_level) => {
return (req, res, next) => {
让user_permission_level = parseInt.jwt.permission_level);
设user_id = req.jwt.user_id;
如果(user_permission_level & required_permission_level) {
返回下一个();
} else {
返回res.状态(403).send();
}
};
};
中间件是通用的. 如果用户权限级别与所需权限级别至少有一位重合, 结果将大于零, and we can let the action proceed; otherwise, HTTP代码403将被返回.
现在,我们需要将身份验证中间件添加到用户的模块路由中 /用户/路线.config.js
:
app.邮报》(' /用户的,
UsersController.insert
]);
app.get(' /用户的,
ValidationMiddleware.validJWTNeeded,
PermissionMiddleware.minimumPermissionLevelRequired(支付),
UsersController.list
]);
app.get(/用户/:userId, (
ValidationMiddleware.validJWTNeeded,
PermissionMiddleware.minimumPermissionLevelRequired(免费),
PermissionMiddleware.onlySameUserOrAdminCanDoThisAction,
UsersController.getById
]);
app.补丁(/用户/:userId, [
ValidationMiddleware.validJWTNeeded,
PermissionMiddleware.minimumPermissionLevelRequired(免费),
PermissionMiddleware.onlySameUserOrAdminCanDoThisAction,
UsersController.patchById
]);
app.删除(/用户/:userId, (
ValidationMiddleware.validJWTNeeded,
PermissionMiddleware.minimumPermissionLevelRequired(管理),
UsersController.removeById
]);
这就是Node的基本开发.REST API. 剩下要做的就是对其进行全面测试.
Insomnia 一个像样的REST客户端有一个好的免费版本吗. 最佳实践是, of course, 在项目中包括代码测试并实现适当的错误报告, 但是当错误报告和调试服务不可用时,第三方REST客户端非常适合测试和实现第三方解决方案. 我们将在这里使用它来扮演应用程序的角色,并深入了解我们的API正在发生什么.
要创建一个用户,我们只需要 POST
将所需字段存储到适当的端点,并存储生成的ID以供后续使用.
API将使用用户ID进行响应:
控件生成JWT /auth/
endpoint:
我们应该得到一个令牌作为响应:
Grab the accessToken
,加上前缀 Bearer
(记住空格),并将其添加到下面的请求标头中 授权
:
如果我们现在不这样做,我们已经实现了权限中间件, 除了注册之外的每个请求都将返回HTTP代码401. 但是,有了有效的令牌之后,我们从 /用户/:userId
:
如前所述, 我们展示所有领域是为了教育目的和简单起见. 密码(散列或其他)永远不应该在响应中可见.
让我们尝试获取用户列表:
Surprise! 我们得到一个403响应.
我们的用户没有访问此端点的权限. 我们需要改变 permissionLevel
我们的用户从1到7(甚至5)都可以, 因为我们的免费和付费权限级别分别表示为1和4, 分别.我们可以在MongoDB中手动做到这一点, 在它的交互提示下, 像这样(将ID更改为您的本地结果):
db.users.update({"_id": ObjectId("5b02c5c84817bf28049e58a3")},{$set:{"permissionLevel":5}})
现在我们需要生成一个新的JWT.
完成之后,我们得到正确的响应:
接下来,让我们通过发送一个 PATCH
请求与我们的一些字段 /用户/:userId
endpoint:
我们期待204的回复,作为行动成功的确认, 但是我们可以再次请求用户验证.
最后,我们需要删除用户. 我们需要如上所述创建一个新用户(不要忘记记录用户ID),并确保为管理用户拥有适当的JWT. 新用户需要将其权限设置为2053(即2048 -)ADMIN
-加上我们前面的5),也能够执行删除操作. 完成这些并生成新的JWT之后,我们必须更新我们的 授权
请求头:
Sending a DELETE
请求 /用户/:userId
,我们应该会得到204的回复作为确认. 我们可以再次通过请求来验证 /users/
从我们的Node API服务器中列出所有现有用户.
使用本教程中介绍的工具和方法,您现在应该能够 创建简单安全的Node.js REST api. 跳过了许多对流程不重要的最佳实践,所以不要忘记:
常见的/ config / env.config.js
到一个非环保的回购 秘密分配机制.读者可以做的最后一个练习是转换Node.从使用JavaScript的API服务器代码库转移到 异步/等待 technique.
对于那些可能有兴趣将他们的JavaScript REST api提升到一个新的水平的人, 我们现在还有 TypeScript版本 该节点的.js API教程项目.
Marcos在IT和开发方面拥有超过15年的经验. 他的爱好包括REST架构、敏捷开发方法和JavaScript.
12
世界级的文章,每周发一次.
世界级的文章,每周发一次.