作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
马科斯·恩里克·达席尔瓦的头像

Marcos Henrique da Silva

Marcos在IT和开发方面拥有超过15年的经验. 他的爱好包括REST架构、敏捷开发方法和JavaScript.

工作经验

12

Share

编者注:本文由我们的编辑团队于2022年12月2日更新. 它已被修改,以包括最近的来源,并与我们目前的编辑标准保持一致.

应用程序编程接口(api)无处不在. 它们使软件能够始终如一地与软件的其他部分(内部或外部)进行通信, 可扩展性的关键因素是什么, not to mention reusability.

如今,在线服务拥有面向公众的api非常普遍. 这使得其他开发人员可以轻松地集成社交媒体登录等功能, 信用卡付款, 行为跟踪. The de facto 他们为此使用的标准称为具象状态转移(REST).

And why build a Node.REST API, in particular? 虽然许多平台和编程语言可以用于类任务 ASP.NET Core, Laravel (PHP), or 瓶(Python)-JavaScript仍然是 最流行的语言 among professional developers. 因此,在本教程中,我们的基本但安全的REST API后端将侧重于常见的组件 JavaScript开发人员:

  • Node.Js,读者应该已经对它比较熟悉了.
  • Express.js, 它极大地简化了构建常见的web服务器任务,并且是构建Node的标准费用.. REST API后端.
  • Mongoose,它将我们的后端连接到MongoDB数据库.

学习本教程的开发人员还应该熟悉终端(或命令提示符)。.

注意:我们不会在这里讨论前端代码库, 但事实上,我们的后端是用JavaScript编写的,这使得共享代码对象模型变得很方便, 例如,在整个堆栈中.

剖析REST API

REST api用于使用一组通用的无状态操作来访问和操作数据. 这些操作是HTTP协议的组成部分,代表了基本的创建, read, update, and delete (CRUD) functionality, 一对一的:虽然不是一对一的方式:

  • POST (创建资源或提供数据)
  • GET (检索资源索引或单个资源)
  • PUT (create or replace a resource)
  • PATCH (update/modify a resource)
  • DELETE (删除资源)

使用这些HTTP操作和一个资源名作为地址,我们可以构建一个Node.通过为每个操作创建一个端点来使用REST API. And by implementing the pattern, 我们将拥有一个稳定且易于理解的基础,使我们能够快速地开发代码并在之后维护它. 同样的基础将用于集成第三方功能, most of which likewise use REST APIs, making such integration faster.

现在,让我们开始创建安全节点.REST API.

在本教程中, 我们将为资源调用创建一个非常通用(并且非常实用)的安全REST API users.

我们的资源将具有以下基本结构:

  • id (an auto-generated UUID)
  • firstName
  • lastName
  • email
  • password
  • permissionLevel (what is this user allowed to do?)

我们将为该资源创建以下操作:

  • POST 在端点上 /users (创建新用户)
  • GET 在端点上 /users (列出所有用户)
  • GET 在端点上 /用户/:userId (获取特定用户)
  • PATCH 在端点上 /用户/:userId (update the data for a specific user)
  • DELETE 在端点上 /用户/:userId (remove a specific user)

我们还将使用JSON web令牌(jwt)作为访问令牌. 为此,我们将创建另一个名为 auth 这将需要用户的电子邮件和密码, in return, 会在某些操作上生成用于身份验证的令牌吗. (Dejan Milosevic’s great article on 用于Java中安全REST应用程序的JWT goes into further detail about this; the principles are the same.)

Node.REST API Tutorial Setup

首先,确保您拥有最新的Node.已安装的Js版本. 对于本文,我将使用版本14.9.0; it may also work on older versions.

Next, make sure that you have MongoDB installed. 我们不会解释这里使用的Mongoose和MongoDB的细节, but to get the basics running, 只需以交互模式启动服务器(例如.e., from the command line as mongo) rather than as a service. 这是因为, at one point in this tutorial, 我们需要直接与MongoDB交互,而不是通过我们的Node.js code.

注意:使用MongoDB, 不需要像在某些RDBMS场景中那样创建特定的数据库. The first insert call from our Node.Js代码会自动触发它的创建.

本教程不包含工作项目所需的所有代码. It’s intended instead that you clone 配套回购 当你通读时,只要跟着要点走就行了. 但是,如果您愿意,也可以根据需要从repo中复制特定的文件和片段.

Navigate to the resulting rest-api-tutorial / 在终端中的文件夹. 你会看到我们的项目包含三个模块文件夹:

  • common (处理所有共享服务,以及用户模块之间共享的信息)
  • users (everything regarding users)
  • auth (处理JWT生成和登录流)

Now, run npm安装 (or yarn 如果你有的话).

祝贺你! 现在,您已经拥有了运行简单Node所需的所有依赖项和设置.. REST API后端.

Creating the User Module

我们将使用 Mongoose,物体 数据建模 (ODM)库,用于在用户模式中创建用户模型.

首先,我们需要在中创建Mongoose模式 /用户/模型/用户.model.js:

const userSchema = new Schema({
   firstName:字符串,
   姓:字符串,
   电子邮件:字符串,
   密码:字符串,
   permissionLevel:数量
});

一旦定义了模式,就可以轻松地将模式附加到用户模型上.

const userModel = mongoose.model('Users', userSchema);

在那之后, 我们可以使用这个模型来实现Express中需要的所有CRUD操作.js端点.

让我们从定义Express的“创建用户”操作开始.j的路由 用户/路线.config.js:

app.邮报》(' /用户的,
   UsersController.insert
]);

This is pulled into our Express.Js应用程序在主 index.js file. The UsersController 对象从控制器导入,在控制器中对密码进行适当的散列,定义为 /users/控制器s/users.控制器.js:

exports.insert = (req, res) => {
   设salt = crypto.randomBytes (16).toString(“base64”);
   让hash = crypto.createHmac('sha512',salt)
                                    .更新(要求.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@ngskmc-eis.net",
   "password" : "s3cr3tp4sswo4rd"
}

有几个工具可以用于此. 我们将在下面介绍失眠,但你也可以使用 Postman 或者像cURL(一个命令行工具)这样的开源替代品 Bruno. 例如,您甚至可以只使用javascript, 从浏览器的内置开发工具控制台中:

fetch('http://localhost:3600/users', {
        方法:“文章”,
        标题:{
            "Content-type": "application/json"
        },
        身体:JSON.stringify ({
            “firstName”:“马科斯”,
            “姓”:“席尔瓦”,
            “电子邮件”:“马科斯.henrique@ngskmc-eis.net",
            "password": "s3cr3tp4sswo4rd"
        })
    })
    .then(function(response) {
        返回响应.json();
    })
    .然后(函数(数据){
        console.log('请求成功,JSON响应',数据);
    })
    .抓住(函数(错误){
        console.log('Request failed', error);
    });

在这一点上,一个有效的帖子的结果将只是来自创建的用户的ID: { "id": "5b02c5c84817bf28049e58a3" }. 我们还需要加上 createUser 方法导入模型 用户/模型/用户.model.js:

exports.createUser = (userData) => {
    const user = new User(userData);
    返回用户.save();
};

Now we need to see if the user exists. 为此,我们将为的实现“获取用户id”特性 用户/:用户标识 endpoint.

First, we create an Express.j的路由 /用户/线路/配置.js:

app.get(/用户/:userId, (
    UsersController.getById
]);

Then, we create the 控制器 in /users/控制器s/users.控制器.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;
        返回结果;
    });
};

The response will look like this:

{
   “firstName”:“马科斯”,
   “姓”:“席尔瓦”,
   “电子邮件”:“马科斯.henrique@ngskmc-eis.net",
   “密码”:“Y + XZEaR7J8xAQCc37nf1rw = = $ p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh + CUQ4n / E0z48mp8SDTpX2ivuQ = = ",
   “permissionLevel”:1、
   "id": "5b02c5c84817bf28049e58a3"
}

注意,我们可以看到散列密码. 对于本教程, we are showing the password, 但最好的做法是永远不要泄露密码, even if it has been hashed. Another thing we can see is the permissionLevel,稍后我们将使用它来处理用户权限.

重复上述模式,我们现在可以添加更新用户的功能. 我们将使用 PATCH 操作,因为它将使我们能够只发送我们想要更改的字段. 的表达.js route will, therefore, be PATCH to /用户/:userid,我们将发送任何我们想要更改的字段. 我们还需要实现一些额外的验证,因为更改应该仅限于有问题的用户或管理员, 并且只有管理员应该能够更改 permissionLevel. 我们现在将跳过它,并在实现auth模块后回到它. 现在,我们的控制器看起来像这样:

exports.patchById = (req, res) => {
   if (req.body.密码){
       设salt = crypto.randomBytes (16).toString(“base64”);
       让hash = crypto.createHmac('sha512', salt).更新(要求.body.password).消化(“base64”);
       req.body.password = salt + "$" + hash;
   }
   UserModel.patchUser(要求.params.userId,要求.body).then((result) => {
           res.状态(204).send({});
   });
};

默认情况下, 我们将发送一个没有响应体的HTTP代码204,以表明请求成功.

And we’ll need to add the 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)
            .exec(function (err, users) {
                If (err) {
                    拒绝(错);
                } else {
                    解决(用户);
                }
            })
    });
};

生成的列表响应将具有以下结构:

[
   {
       “firstName”:“马可”,
       “姓”:“席尔瓦”,
       “电子邮件”:“马科斯.henrique@ngskmc-eis.net",
       “密码”:“z4tS / DtiH + 0 gb4j6qn1k3w = = $ al6sGxKBKqxRQkDmhnhQpEB6 + DQgDRH2qr47BZcqLm4 / fphZ7 + a9U + HhxsNaSnGB2l05Oem / BLIOkbtOuw1tXA = = ",
       “permissionLevel”:1、
       "id": "5b02c5c84817bf28049e58a3"
   },
   {
       “firstName”:“保罗”,
       “姓”:“席尔瓦”,
       “电子邮件”:“马科斯.henrique2@ngskmc-eis.net",
       “密码”:“wTsqO1kHuVisfDIcgl5YmQ = = $ cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw = = ",
       “permissionLevel”:1、
       "id": "5b02d038b653603d1ca69729"
   }
]

最后要实现的部分是 DELETE at /用户/:userId.

Our 控制器 for deletion will be:

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模式的核心概念. 我们需要返回到这段代码来实现对它的一些验证和权限, 但首先我们需要开始建立我们的安全. Let’s create the auth module.

Creating the Auth Module

Before we can secure the users 模块通过实现权限和验证中间件, 我们需要能够为当前用户生成有效的令牌. 我们将生成一个JWT,以响应提供有效电子邮件和密码的用户. JWT允许用户安全地发出多个请求,而无需重复验证. It usually has an expiration time, 为了保证通信安全,每隔几分钟就会重新创建一个新的令牌. 对于本教程, though, 我们将放弃刷新令牌,并保持每次登录单个令牌的简单性.

First, we will create an endpoint for POST 请求 /auth resource. 请求正文将包含用户的电子邮件和密码:

{
   "电邮":"马科斯.henrique2@ngskmc-eis.net",
   "password" : "s3cr3tp4sswo4rd2"
}

在使用控制器之前,我们应该验证用户 /authorization/中间件s/verify.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', salt)
                                .更新(要求.body.password)
                                .消化(“base64”);
               if (hash === passwordFields[1]) {
                   req.body = {
                       用户名:用户[0]._id,
                       电子邮件:用户[0].email,
                       permissionLevel: user[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', salt).更新(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. 以下是我们需要处理的场景:

  • Public用于创建用户(注册过程). We will not use JWT for this scenario.
  • 用于登录用户和管理员更新该用户.
  • Private for admin,仅用于删除用户帐户.

Having identified these scenarios, 我们首先需要一个中间件,它总是验证用户是否使用了有效的JWT. 中间件 /common/中间件s/auth.验证.中间件.js 可以这么简单:

exports.validJWTNeeded = (req, res, next) => {
    if (req.headers['authorization']) {
        try {
            让authorization = req.headers['authorization'].分割(' ');
            如果(授权[0] !== '承载者'){
                返回res.状态(401).send();
            } else {
                req.jwt = jwt.verify(authorization[1], secret);
                返回下一个();
            }
        } catch (err) {
            返回res.状态(403).send();
        }
    } else {
        返回res.状态(401).send();
    }
}; 

我们将使用HTTP错误码来处理请求错误:

  • HTTP 401 for an invalid request
  • HTTP 403用于无效令牌的有效请求,或无效权限的有效令牌

我们可以使用位与运算符(位掩码)来控制权限. 如果我们将每个需要的权限设置为2的幂, 我们可以将32位整数的每一位视为单个权限. 通过将权限值设置为2147483647,管理员可以拥有所有权限. 然后,该用户可以访问任何路由. 另一个例子, 权限值设置为7的用户将对值为1的位标记的角色具有权限, 2, and 4 (two to the power of 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;
       if (user_permission_level & required_permission_level) {
           返回下一个();
       } else {
           返回res.状态(403).send();
       }
   };
};

The 中间件 is generic. 如果用户权限级别与所需权限级别至少有一位重合, the result will be greater than zero, and we can let the action proceed; otherwise, the HTTP code 403 will be returned.

现在,我们需要将身份验证中间件添加到用户的模块路由中 /用户/路线.config.js:

app.邮报》(' /用户的,
   UsersController.insert
]);
app.get(' /用户的,
   ValidationMiddleware.validJWTNeeded,
   PermissionMiddleware.minimumPermissionLevelRequired(PAID),
   UsersController.list
]);
app.get(/用户/:userId, (
   ValidationMiddleware.validJWTNeeded,
   PermissionMiddleware.minimumPermissionLevelRequired(FREE),
   PermissionMiddleware.onlySameUserOrAdminCanDoThisAction,
   UsersController.getById
]);
app.patch('/用户/:userId', [
   ValidationMiddleware.validJWTNeeded,
   PermissionMiddleware.minimumPermissionLevelRequired(FREE),
   PermissionMiddleware.onlySameUserOrAdminCanDoThisAction,
   UsersController.patchById
]);
app.delete('/用户/:userId', [
   ValidationMiddleware.validJWTNeeded,
   PermissionMiddleware.minimumPermissionLevelRequired(管理),
   UsersController.removeById
]);

这就是Node的基本开发.REST API. 剩下要做的就是对其进行全面测试.

Running and Testing with Insomnia

Insomnia 一个像样的REST客户端有一个好的免费版本吗. 最佳实践是, of course, 在项目中包括代码测试并实现适当的错误报告, 但是当错误报告和调试服务不可用时,第三方REST客户端非常适合测试和实现第三方解决方案. 我们将在这里使用它来扮演应用程序的角色,并深入了解我们的API正在发生什么.

To create a user, we just need to POST 将所需字段存储到适当的端点,并存储生成的ID以供后续使用.

请求中包含用于创建用户的适当数据

The API will respond with the user ID:

Confirmation response with userID

We can now generate the JWT using the /auth/ endpoint:

带有登录数据的请求

We should get a token as our response:

包含相应JSON Web令牌的确认

Grab the accessToken,加上前缀 Bearer (记住空格),并将其添加到下面的请求标头中 授权:

设置要传输的头包含身份验证JWT

如果我们现在不这样做,我们已经实现了权限中间件, 除了注册之外的每个请求都将返回HTTP代码401. 但是,有了有效的令牌之后,我们从 /用户/:userId:

响应中列出了指定用户的数据

如前所述, 我们展示所有领域是为了教育目的和简单起见. 密码(散列或其他)永远不应该在响应中可见.

Let’s try to get a list of users:

Request for a list of all users

Surprise! 我们得到一个403响应.

由于缺乏适当的权限级别,操作被拒绝

我们的用户没有访问此端点的权限. We will need to change the permissionLevel 我们的用户从1到7(甚至5)都可以, 因为我们的免费和付费权限级别分别表示为1和4, 分别.) We can do this manually in MongoDB, at its interactive prompt, 像这样(将ID更改为您的本地结果):

db.users.update({"_id": ObjectId("5b02c5c84817bf28049e58a3")},{$set:{"permissionLevel":5}})

Now we need to generate a new JWT.

完成之后,我们得到正确的响应:

Response with all users and their data

接下来,让我们通过发送一个 PATCH request with some fields to our /用户/:userId endpoint:

包含要更新的部分数据的请求

我们期待204的回复,作为行动成功的确认, 但是我们可以再次请求用户验证.

Response after successful change

Finally, we need to delete the user. 我们需要如上所述创建一个新用户(不要忘记记录用户ID),并确保为管理用户拥有适当的JWT. 新用户需要将其权限设置为2053(即2048 -)ADMIN-加上我们前面的5),也能够执行删除操作. 完成这些并生成新的JWT之后,我们必须更新我们的 授权 请求头:

Request setup for deleting a user

Sending a DELETE 请求 /用户/:userId,我们应该会得到204的回复作为确认. We can, again, verify by requesting /users/ 从我们的Node API服务器中列出所有现有用户.

Node.js API Server Tutorial: Next Steps

使用本教程中介绍的工具和方法,您现在应该能够 create simple and secure Node.js REST api. 跳过了许多对流程不重要的最佳实践,所以不要忘记:

  • Implement proper 验证s (e.g., make sure that user email is unique).
  • 实现单元测试和错误报告.
  • 禁止用户更改自己的权限级别.
  • 防止管理员自我删除.
  • 防止泄露敏感资料(例如.g.,散列密码).
  • Move the JWT secret from 常见的/ config / env.config.js to an off-repo, non-environment-based secret distribution mechanism.

读者可以做的最后一个练习是转换Node.从使用JavaScript的API服务器代码库转移到 异步/等待 technique.

对于那些可能有兴趣将他们的JavaScript REST api提升到一个新的水平的人, 我们现在还有 TypeScript版本 该节点的.js API教程项目.

就这一主题咨询作者或专家.
预约电话
马科斯·恩里克·达席尔瓦的头像
Marcos Henrique da Silva

位于 Lecco, Province of Lecco, Italy

成员自 2017年2月25日

作者简介

Marcos在IT和开发方面拥有超过15年的经验. 他的爱好包括REST架构、敏捷开发方法和JavaScript.

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

工作经验

12

World-class articles, delivered weekly.

Subscription implies consent to our 隐私政策

World-class articles, delivered weekly.

Subscription implies consent to our 隐私政策

Toptal开发者

加入总冠军® community.