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

马科斯·恩里克·达席尔瓦

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

工作经验

12

Share

编者注:本文由我们的编辑团队于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开发人员:

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

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

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

剖析REST API

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.REST API教程设置

首先,确保您拥有最新的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@4dian8.com",
   "password": " s3cr3tp4ssw4rd "
}

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

fetch (http://localhost: 3600 /用户,{
        方法:“文章”,
        标题:{
            “内容类型”:“application / json”
        },
        身体:JSON.stringify ({
            “firstName”:“马科斯”,
            “姓”:“席尔瓦”,
            “电子邮件”:“马科斯.henrique@4dian8.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@4dian8.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@4dian8.com",
       “密码”:“z4tS / DtiH + 0 gb4j6qn1k3w = = $ al6sGxKBKqxRQkDmhnhQpEB6 + DQgDRH2qr47BZcqLm4 / fphZ7 + a9U + HhxsNaSnGB2l05Oem / BLIOkbtOuw1tXA = = ",
       “permissionLevel”:1、
       “id”:“5 b02c5c84817bf28049e58a3”
   },
   {
       “firstName”:“保罗”,
       “姓”:“席尔瓦”,
       “电子邮件”:“马科斯.henrique2@4dian8.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@4dian8.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. 以下是我们需要处理的场景:

  • Public用于创建用户(注册过程). 我们不会在这个场景中使用JWT.
  • 用于登录用户和管理员更新该用户.
  • Private for admin,仅用于删除用户帐户.

确定了这些场景之后, 我们首先需要一个中间件,它总是验证用户是否使用了有效的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错误码来处理请求错误:

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

我们可以使用位与运算符(位掩码)来控制权限. 如果我们将每个需要的权限设置为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进行响应:

带有userID的确认响应

控件生成JWT /auth/ endpoint:

带有登录数据的请求

我们应该得到一个令牌作为响应:

包含相应JSON Web令牌的确认

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

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

如果我们现在不这样做,我们已经实现了权限中间件, 除了注册之外的每个请求都将返回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 API服务器教程:下一步

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

  • 实现适当的验证(例如.g.,确保用户邮箱是唯一的).
  • 实现单元测试和错误报告.
  • 禁止用户更改自己的权限级别.
  • 防止管理员自我删除.
  • 防止泄露敏感资料(例如.g.,散列密码).
  • 将JWT秘密从 常见的/ config / env.config.js 到一个非环保的回购 秘密分配机制.

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

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

聘请Toptal这方面的专家.
Hire Now
马科斯·恩里克·达席尔瓦的头像
马科斯·恩里克·达席尔瓦

位于 莱科,意大利莱科省

成员自 2017年2月25日

作者简介

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

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

工作经验

12

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

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

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

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

Toptal开发者

加入总冠军® community.