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