应用程序编程接口 (API) 无处不在。它们使软件能够与其他软件部分(内部或外部)一致地进行通信,这是可扩展性的关键因素,更不用说可重用性了。
如今,在线服务拥有面向公众的 API 已经很普遍了。这些使其他开发人员能够轻松集成社交媒体登录、信用卡支付和行为跟踪等功能。在事实上,他们使用这个标准被称为具象状态传输(REST)。
Node.js如何创建REST API?虽然可以使用多种平台和编程语言来完成这项任务——例如,ASP.NET Core、Laravel (PHP)或Bottle (Python) ——在本教程中,我们将构建一个基本但安全的 REST API 后端使用以下栈:
- Node.js,读者应该已经熟悉了
- Express,它极大地简化了在 Node.js 下构建常见的 Web 服务器任务,并且是构建 REST API 后端的标准费用
- Mongoose,它将我们的后端连接到 MongoDB 数据库
遵循本教程的开发人员也应该对终端(或命令提示符)感到满意,本文包括详细的Node.js创建REST API示例。
注意:我们不会在这里介绍前端代码库,但我们的后端是用 JavaScript 编写的这一事实使得在整个堆中共享代码(例如对象模型)很方便。
Node.js创建REST API教程:REST API 剖析
REST API 用于使用一组通用的无状态操作来访问和操作数据。这些操作是 HTTP 协议不可或缺的一部分,代表了基本的创建、读取、更新和删除 (CRUD) 功能,尽管不是以干净的一对一方式:
POST
(创建资源或一般提供数据)GET
(检索资源索引或单个资源)PUT
(创建或替换资源)PATCH
(更新/修改资源)DELETE
(删除资源)
使用这些 HTTP 操作和一个资源名称作为地址,我们可以通过为每个操作创建一个端点来构建一个 REST API。通过实施该模式,我们将拥有一个稳定且易于理解的基础,使我们能够快速发展代码并在之后对其进行维护。如前所述,将使用相同的基础来集成第三方功能,其中大多数同样使用 REST API,使此类集成更快。
现在,让我们开始使用 Node.js 创建我们的安全 REST API!
在本教程中,我们将为名为users
.
我们的资源将具有以下基本结构:
id
(自动生成的 UUID)firstName
lastName
email
password
permissionLevel
(该用户可以做什么?)
我们将为该资源创建以下操作:
POST
在端点上/users
(创建一个新用户)GET
在端点上/users
(列出所有用户)GET
在端点上/users/:userId
(获取特定用户)PATCH
在端点上/users/:userId
(更新特定用户的数据)DELETE
在端点上/users/:userId
(删除特定用户)
我们还将使用 JSON Web 令牌 (JWT) 作为访问令牌。为此,我们将创建另一个名为的资源auth
,该资源需要用户的电子邮件和密码,作为回报,将生成用于某些操作的身份验证的令牌。(Dejan Milosevic 关于JWT 用于 Java 中的安全 REST 应用程序的伟大文章对此进行了更详细的介绍;原理是相同的。)
REST API 教程设置
首先,确保你安装了最新的 Node.js 版本。对于本文,我将使用 14.9.0 版本;它也可能适用于旧版本。
接下来,确保你已安装MongoDB。我们不会解释这里使用的 Mongoose 和 MongoDB 的具体细节,但为了让基础运行起来,只需以交互模式(即,从命令行 as mongo
)而不是作为服务启动服务器。这是因为,在本教程的某个时刻,我们需要直接与 MongoDB 交互,而不是通过我们的 Node.js 代码。
注意:使用 MongoDB,无需像在某些 RDBMS 场景中那样创建特定的数据库。Node.js 代码中的第一个插入调用将自动触发它的创建。
本教程不包含工作项目所需的所有代码。相反,你打算克隆配套存储库,并在阅读时简单地按照重点进行操作 - 但如果你愿意,也可以根据需要从存储库中复制特定文件和片段。
导航到rest-api-tutorial/
终端中的结果文件夹。你会看到我们的项目包含三个模块文件夹:
common
(处理所有共享服务,以及用户模块之间共享的信息)users
(关于用户的一切)auth
(处理 JWT 生成和登录流程)
现在,运行npm install
(或者yarn
如果你有的话。)
恭喜,你现在拥有运行我们简单的 REST API 后端所需的所有依赖项和设置。
创建用户模块
Node.js如何创建REST API?我们将使用Mongoose,一个用于 MongoDB 的对象数据建模 (ODM) 库,在用户模式中创建用户模型。
首先,我们需要在以下位置创建 Mongoose 模式/users/models/users.model.js
:
const userSchema = new Schema({
firstName: String,
lastName: String,
email: String,
password: String,
permissionLevel: Number
});
一旦我们定义了模式,我们就可以轻松地将模式附加到用户模型上。
const userModel = mongoose.model('Users', userSchema);
之后,我们可以使用此模型在 Express 端点中实现我们想要的所有 CRUD 操作。
让我们从“创建用户”操作开始,在 中定义路由users/routes.config.js
:
app.post('/users', [
UsersController.insert
]);
这在主index.js
文件中被拉入我们的 Express 应用程序。该UsersController
对象是从我们的控制器导入的,我们在其中适当地散列密码,定义在/users/controllers/users.controller.js
:
exports.insert = (req, res) => {
let salt = crypto.randomBytes(16).toString('base64');
let hash = crypto.createHmac('sha512',salt)
.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});
});
};
此时,我们可以通过运行服务器 ( npm start
) 并使用一些 JSON 数据发送POST
请求来测试我们的 Mongoose 模型/users
:
{
"firstName" : "Marcos",
"lastName" : "Silva",
"email" : "marcos.henrique@toptal.com",
"password" : "s3cr3tp4sswo4rd"
}
Node.js创建REST API示例:有几种工具可以用于此目的。Insomnia(下文介绍)和 Postman 是流行的 GUI 工具,curl
也是常见的 CLI 选择。你甚至可以只使用 JavaScript,例如,从浏览器的内置开发工具控制台:
fetch('http://localhost:3600/users', {
method: 'POST',
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
"firstName": "Marcos",
"lastName": "Silva",
"email": "marcos.henrique@toptal.com",
"password": "s3cr3tp4sswo4rd"
})
})
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log('Request succeeded with JSON response', data);
})
.catch(function(error) {
console.log('Request failed', error);
});
此时,有效帖子的结果将只是来自创建的用户的 id: { "id": "5b02c5c84817bf28049e58a3" }
。我们还需要将createUser
方法添加到模型中users/models/users.model.js
:
exports.createUser = (userData) => {
const user = new User(userData);
return user.save();
};
一切就绪,现在我们需要查看用户是否存在。为此,我们将实施以下端点“获得通过ID用户”功能:users/:userId
。
首先,我们在 中创建一个路由/users/routes/config.js
:
app.get('/users/:userId', [
UsersController.getById
]);
然后,我们在/users/controllers/users.controller.js
以下位置创建控制器:
exports.getById = (req, res) => {
UserModel.findById(req.params.userId).then((result) => {
res.status(200).send(result);
});
};
最后,将findById
方法添加到模型中/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@toptal.com",
"password": "Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==",
"permissionLevel": 1,
"id": "5b02c5c84817bf28049e58a3"
}
请注意,我们可以看到散列的密码。在本教程中,我们将显示密码,但显而易见的最佳做法是永远不要泄露密码,即使它已被散列。我们可以看到的另一件事是permissionLevel
,稍后我们将使用它来处理用户权限。
重复上面列出的模式,我们现在可以添加更新用户的功能。我们将使用该PATCH
操作,因为它将使我们能够仅发送我们想要更改的字段。这条路线会,因此,是PATCH
到/users/:userid
了,我们会送我们想改变任何领域。我们还需要实施一些额外的验证,因为更改应该仅限于相关用户或管理员,并且只有管理员才能更改permissionLevel
. 我们暂时跳过它,一旦我们实现了 auth 模块,我们就会回到它。现在,我们的控制器看起来像这样:
exports.patchById = (req, res) => {
if (req.body.password){
let salt = crypto.randomBytes(16).toString('base64');
let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64");
req.body.password = salt + "$" + hash;
}
UserModel.patchUser(req.params.userId, req.body).then((result) => {
res.status(204).send({});
});
};
默认情况下,我们将发送一个没有响应正文的 HTTP 代码 204 来表示请求成功。
我们需要将patchUser
方法添加到模型中:
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)
.exec(function (err, users) {
if (err) {
reject(err);
} else {
resolve(users);
}
})
});
};
生成的列表响应将具有以下结构:
[
{
"firstName": "Marco",
"lastName": "Silva",
"email": "marcos.henrique@toptal.com",
"password": "z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==",
"permissionLevel": 1,
"id": "5b02c5c84817bf28049e58a3"
},
{
"firstName": "Paulo",
"lastName": "Silva",
"email": "marcos.henrique2@toptal.com",
"password": "wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==",
"permissionLevel": 1,
"id": "5b02d038b653603d1ca69729"
}
]
最后要实现的部分是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);
}
});
});
};
Node.js创建REST API教程:我们现在拥有操作用户资源所需的所有操作,并且我们完成了用户控制器。这段代码的主要思想是为你提供使用 REST 模式的核心概念。我们需要返回此代码以对其实施一些验证和权限,但首先,我们需要开始构建我们的安全性。让我们创建 auth 模块。
创建身份验证模块
Node.js创建REST API示例:在我们users
通过实现权限和验证中间件来保护模块之前,我们需要能够为当前用户生成一个有效的令牌。我们将生成 JWT 以响应提供有效电子邮件和密码的用户。JWT 是一个非凡的 JSON Web 令牌,你可以使用它让用户安全地发出多个请求,而无需重复验证。它通常有一个到期时间,每隔几分钟就会重新创建一个新令牌以确保通信安全。但是,对于本教程,我们将放弃刷新令牌,并通过每次登录使用单个令牌来保持简单。
Node.js如何创建REST API?首先,我们将为资源POST
请求创建一个端点/auth
。请求正文将包含用户电子邮件和密码:
{
"email" : "marcos.henrique2@toptal.com",
"password" : "s3cr3tp4sswo4rd2"
}
在我们使用控制器之前,我们应该验证用户/authorization/middlewares/verify.user.middleware.js
:
exports.isPasswordAndUserMatch = (req, res, next) => {
UserModel.findByEmail(req.body.email)
.then((user)=>{
if(!user[0]){
res.status(404).send({});
}else{
let passwordFields = user[0].password.split('$');
let salt = passwordFields[0];
let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64");
if (hash === passwordFields[1]) {
req.body = {
userId: user[0]._id,
email: user[0].email,
permissionLevel: user[0].permissionLevel,
provider: 'email',
name: user[0].firstName + ' ' + user[0].lastName,
};
return next();
} else {
return res.status(400).send({errors: ['Invalid email or password']});
}
}
});
};
完成后,我们可以转到控制器并生成 JWT:
exports.login = (req, res) => {
try {
let refreshId = req.body.userId + jwtSecret;
let salt = crypto.randomBytes(16).toString('base64');
let hash = crypto.createHmac('sha512', salt).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});
}
};
尽管我们不会在本教程中刷新令牌,但已设置控制器以启用此类生成,以便在后续开发中更容易实现它。
我们现在需要的只是创建路由并在/authorization/routes.config.js
以下位置调用适当的中间件:
app.post('/auth', [
VerifyUserMiddleware.hasAuthValidFields,
VerifyUserMiddleware.isPasswordAndUserMatch,
AuthorizationController.login
]);
响应将在 accessToken 字段中包含生成的 JWT:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY",
"refreshToken": "U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ=="
}
创建令牌后,我们可以使用Authorization
表单在标题中使用它Bearer ACCESS_TOKEN
。
创建权限和验证中间件
我们应该定义的第一件事是谁可以使用users
资源。这些是我们需要处理的场景:
- 公共用于创建用户(注册过程)。我们不会在这种情况下使用 JWT。
- 登录用户和管理员更新该用户的私有信息。
- 管理员专用,仅用于删除用户帐户。
确定了这些场景后,我们首先需要一个中间件,如果用户使用有效的 JWT,它总是可以验证用户。中间件/common/middlewares/auth.validation.middleware.js
可以很简单:
exports.validJWTNeeded = (req, res, next) => {
if (req.headers['authorization']) {
try {
let authorization = req.headers['authorization'].split(' ');
if (authorization[0] !== 'Bearer') {
return res.status(401).send();
} else {
req.jwt = jwt.verify(authorization[1], secret);
return next();
}
} catch (err) {
return res.status(403).send();
}
} else {
return res.status(401).send();
}
};
我们将使用 HTTP 错误代码来处理请求错误:
- 无效请求的 HTTP 401
- HTTP 403 用于具有无效令牌的有效请求,或具有无效权限的有效令牌
我们可以使用按位与运算符(位掩码)来控制权限。如果我们将每个必需的权限设置为 2 的幂,我们可以将 32 位整数的每一位视为单个权限。然后,管理员可以通过将其权限值设置为 2147483647 来拥有所有权限。然后该用户可以访问任何路由。作为另一个示例,权限值设置为 7 的用户将拥有对用值 1、2 和 4(2 的 0、1 和 2 的幂)标记的角色的权限。
中间件看起来像这样:
exports.minimumPermissionLevelRequired = (required_permission_level) => {
return (req, res, next) => {
let user_permission_level = parseInt(req.jwt.permission_level);
let user_id = req.jwt.user_id;
if (user_permission_level & required_permission_level) {
return next();
} else {
return res.status(403).send();
}
};
};
中间件是通用的。如果用户权限级别和所需权限级别至少有一位重合,则结果将大于零,我们可以让动作继续;否则,将返回 HTTP 代码 403。
现在,我们需要将身份验证中间件添加到用户的模块路由中/users/routes.config.js
:
app.post('/users', [
UsersController.insert
]);
app.get('/users', [
ValidationMiddleware.validJWTNeeded,
PermissionMiddleware.minimumPermissionLevelRequired(PAID),
UsersController.list
]);
app.get('/users/:userId', [
ValidationMiddleware.validJWTNeeded,
PermissionMiddleware.minimumPermissionLevelRequired(FREE),
PermissionMiddleware.onlySameUserOrAdminCanDoThisAction,
UsersController.getById
]);
app.patch('/users/:userId', [
ValidationMiddleware.validJWTNeeded,
PermissionMiddleware.minimumPermissionLevelRequired(FREE),
PermissionMiddleware.onlySameUserOrAdminCanDoThisAction,
UsersController.patchById
]);
app.delete('/users/:userId', [
ValidationMiddleware.validJWTNeeded,
PermissionMiddleware.minimumPermissionLevelRequired(ADMIN),
UsersController.removeById
]);
我们的 REST API 的基本开发到此结束。剩下要做的就是全部测试。
Node.js创建REST API教程:运行和测试REST
Node.js如何创建REST API?Insomnia是一个不错的 REST 客户端,有一个很好的免费版本。当然,最佳实践是在项目中包含代码测试并实施正确的错误报告,但是当错误报告和调试服务不可用时,第三方 REST 客户端非常适合测试和实施第三方解决方案。我们将在这里使用它来扮演应用程序的角色,并深入了解我们的 API 发生了什么。
要创建用户,我们只需要将POST
必填字段添加到相应的端点并存储生成的 ID 以供后续使用。
API 将使用用户 ID 进行响应:
我们现在可以使用/auth/
端点生成 JWT :
我们应该得到一个令牌作为我们的回应:
获取accessToken
,加上前缀Bearer
(记住空格),然后将其添加到 下的请求标头中Authorization
:
如果现在我们已经实现了权限中间件,如果我们不这样做,除了注册之外的每个请求都将返回 HTTP 代码 401。但是,有了有效的令牌,我们会得到以下响应/users/:userId
:
此外,如前所述,出于教育目的和简单起见,我们将显示所有字段。密码(散列或其他方式)不应在响应中可见。
让我们尝试获取用户列表:
惊喜!我们收到 403 响应。
我们的用户无权访问此端点。我们需要将permissionLevel
用户的权限从 1更改为 7(甚至 5 也可以,因为我们的免费和付费权限级别分别表示为 1 和 4。)我们可以在 MongoDB 中的交互式提示下手动执行此操作,像这样(将 ID 更改为你的本地结果):
db.users.update({"_id" : ObjectId("5b02c5c84817bf28049e58a3")},{$set:{"permissionLevel":5}})
然后,我们需要生成一个新的 JWT。
完成后,我们得到正确的响应:
接下来,让我们通过向端点发送PATCH
包含一些字段的请求来测试更新功能/users/:userId
:
我们期望 204 响应作为成功操作的确认,但我们可以再次请求用户进行验证。
Node.js创建REST API示例:最后,我们需要删除用户。我们需要如上所述创建一个新用户(不要忘记记下用户 ID),并确保我们拥有适合管理员用户的 JWT。新用户需要将他们的权限设置为 2053(即 2048——ADMIN
加上我们之前的 5)才能执行删除操作。完成并生成新的 JWT 后,我们必须更新我们的Authorization
请求标头:
向 发送DELETE
请求/users/:userId
,我们应该得到 204 响应作为确认。我们可以再次通过请求/users/
列出所有现有用户来验证。
Node.js创建REST API教程的后续步骤
使用本教程中介绍的工具和方法,你现在应该能够在 Node.js 上创建简单且安全的 REST API。许多对流程不重要的最佳实践被跳过,所以不要忘记:
- 实施适当的验证(例如,确保用户电子邮件是唯一的)
- 实施单元测试和错误报告
- 防止用户更改自己的权限级别
- 防止管理员删除自己
- 防止泄露敏感信息(例如,散列密码)
- 将 JWT 秘密从
common/config/env.config.js
非存储库、非基于环境的秘密分发机制移至
读者的最后一项练习是将代码库从使用 JavaScript 承诺转换为 异步/等待技术。
对于那些可能感兴趣的人,现在还提供了该项目的 TypeScript 版本。