如何在JavaScript中使用BcryptsJS安全处理密码

介绍

保护网站密码是任何开发人员都应具备的基本技能。JavaScript提供了一种选项,可以利用JavaScriptBcryptJS模块提供的哈希算法,确保密码或其他敏感数据的安全存储和处理。

在本教程中,您将学习有关BcryptJS和哈希的知识,以设置一个基本的express服务器,将密码以哈希形式存储在数据库中,而不是以原始字符串形式,并检索它们以验证密码。

先决条件

要继续进行本教程,您应具备以下设置。

  1. 在您的计算机上安装了稳定版本的Node.js,版本为12.x或更高版本。您可以使用 DigitalOcean 教程来安装最新的Node.js版本。

  2. 您应了解如何使用JavaScript编码

  3. _你的计算机上应该已安装 Express JS。你可以使用这篇指南来学习如何设置 Express 服务器。

  4. 最后,你需要 MongoDB 社区版或 Atlas 数据库来完成本教程。你可以使用以下 DigitalOcean 指南之一来_ 安装 MongoDB

为什么使用 BcryptJS?

Bcrypt 是一种哈希算法,用于为密码创建哈希以在数据泄露时存储它们。这种高级哈希算法使用盐,使得它难以被诸如暴力破解之类的攻击所破解。

BcryptJS是Bcrypt哈希算法的JavaScript实现,允许您使用哈希加密而无需涉及复杂的哈希函数。使BcryptJS成为密码安全的不错选择的原因如下:

  1. 安全性 – BcryptJS实现了Bcrypt算法,这是一种缓慢的算法(在哈希方面是一件好事),需要强大的计算能力。这使得攻击者破解密码哈希变得十分困难,即使在数据泄露的情况下也能确保密码的安全。

  2. 盐值 – BcryptJS处理密码的随机盐值生成,以确保存储安全性(我们将在下一节更详细地了解哈希和盐值)。盐值使得相对较弱密码的哈希变得更复杂,使其更难以解密。

  3. 易于使用 – BcryptJS为JavaScript开发人员提供了一个工具,可以加密他们的密码,而无需深入了解哈希算法。

在下一步中,我们将简要了解哈希和盐。

在将这些概念应用于您的项目之前,您必须了解哈希和盐的工作原理。

哈希是将简单字符串或明文转换为一串随机字符(加密)。这允许安全地存储和/或传输敏感数据。哈希包括以下关键步骤:

首先,存储可能是任何类型(二进制、字符、十进制等)的数据,作为纯文本或字符串。哈希函数 – 哈希函数是一个数学算法,它从数据中获取输入并将其转换为一组字符或哈希代码。哈希函数是确定性的(对于相同的输入产生相同的输出)和单向函数(这意味着几乎不可能将哈希函数的输出反向工程化,即将哈希转换为其输入数据)。

碰撞抵抗 – 这意味着哈希函数的创建考虑到了碰撞抵抗的概念,即两个不同的输入不能产生相同的输出(哈希码)。

身份验证 – 哈希函数是确定性的,对于相同的输入产生相同的哈希。因此,在对存储为哈希的密码进行身份验证时,一般的想法是,如果要验证的密码与存储在数据库中的哈希匹配,那么密码是正确的。

加盐

由于哈希已经存在几十年了,出现了一些发展,比如彩虹表,其中包含数十亿个数据条目,包含基于不同哈希算法的数据字符串及其相应的哈希。

现在,考虑这样一种情况,一个在您网站上创建帐户的用户使用了弱密码。因此,在发生数据泄露的情况下,攻击者可以查找用户哈希的匹配项,并找到具有弱密码的用户帐户的哈希。在高安全性应用中,这将是灾难性的。为了防止这种情况发生,使用了盐。

加盐是通过在将密码的哈希存储到数据库之前向其添加一串随机字符的方式,为哈希添加的额外安全层。因此,即使数据在泄露中被泄露,攻击者也很难解密包含盐的哈希。考虑以下示例:

  1. Password = ‘sammy’
  2. Hash = £%$^&£!23!3%!!
  3. Salt = 2vqw£4Df$%sdfk
  4. Hash + Salt = £%$^&£!23!3%!!2vqw£4Df$%sdfk

正如我们可以清楚地看到的那样,使用盐存储的密码由于哈希的确定性质,更不太可能被破解。因此,如果攻击者例如在 rainbow 表中查找这个哈希+密码字符串,他将得不到实际的密码字符串,而是完全不同的东西。

现在,您可以准备使用BcryptJS以行业标准的方式保护您的密码。

安装 BcryptJS 和其他所需模块

既然您了解了哈希和盐的知识,您只需拿起电脑开始编码。项目结构将如下:

Project Structure

首先,我们将通过以下步骤创建一个 npm 项目:

  1. 打开一个文件夹并创建一个名为app.js的文件。

  2. 在此文件夹中打开一个终端窗口并键入命令

npm init

之后,会要求您提供输入,但可以按 Enter 键而不提供任何输入。

  1. 然后,创建另外 3 个文件,分别为
  • auth.js
  • db.js
  • User.js
  1. 在同一个终端窗口中,键入以下命令以安装必要的软件包。
npm install express mongoose BcryptJS nodemon

你现在有一个完整的项目环境设置,可以按照本教程进行操作。在接下来的步骤中,您将学习如何创建一个服务器,使用BcryptJS来安全存储和验证MongoDB中的密码。

使用Express JS设置服务器

既然您已经设置好项目结构,可以创建一个服务器,该服务器使用bcrytjs通过将密码存储为哈希值来保护它们,并进行身份验证。我们将按照以下正确的步骤创建服务器。

第1步 – 连接到MongoDB数据库

为了连接到mongoDB,我们使用社区版。为了保持项目的组织性,您将保存设置连接的代码在db.js文件中。

  1. const mongoose = require("mongoose");
  2. const mongoURI = "mongodb://127.0.0.1:27017/bcrypt_database";
  3. const connectMongo = async () => {
  4. try {
  5. await mongoose.connect(mongoURI);
  6. console.log("Connected to MongoDB!");
  7. } catch (error) {
  8. console.error("Error connecting to MongoDB: ", error.message);
  9. }
  10. };
  11. module.exports = connectMongo;

在这里,您导入了mongoose包,该包提供了一个API,将JavaScript连接到MongoDB。此外,在这种情况下,连接URI是针对MongoDB的本地安装的,如果您使用云数据库(如Atlas),您只需更改URI为特定数据库的URI即可。

一个 URI 类似于服务器 URL,不同之处在于 URI 能够识别资源的名称、标识以及它们在互联网上的位置。相比之下,URL 是 URI 的子集,只能标识资源的位置。

connectToMongo 函数是一个 异步 函数,因为 mongoose.connect 返回一个 JavaScript Promise。该函数将在成功执行时返回一个连接。否则,它将返回一个错误。

最后,当 db.js 模块被导入时,我们使用 module.exports 导出这个函数。

第二步 – 创建用户模式

您需要一个基本的模式来创建或验证 MongoDB 数据库中的用户。如果您不知道什么是模式,您可以使用这个优秀的 DO 指南来了解并创建 MongoDB 中的模式。我们只会使用两个字段来定义我们的模式,emailpassword

在您的 User.js 模块中使用以下代码。

  1. const mongoose = require("mongoose");
  2. const UserSchema = new mongoose.Schema({
  3. email:{
  4. type:String,
  5. required:true,
  6. unique:true
  7. },
  8. password:{
  9. type:String,
  10. required:true
  11. }
  12. });
  13. module.exports = mongoose.model('user', UserSchema)

我们在此模式中创建了两个字段,emailpassword。它们都是必需字段,数据类型为字符串。另外,email 是一个唯一字段,意味着一个电子邮件只能在此服务器上的一个账户中使用一次。

最后,我们导出一个名为 users 的模型。模型代表了数据库端的模式。您可以将模式视为定义模型的规则,而模型则存储在 MongoDB 数据库的集合中。

您可以使用 mongoose 库的 model() 函数将模式转换为模型。

第三步 – 在 app.js 中设置服务器

在执行上述步骤后,您已成功创建了一个模型和一个模块,用于连接到 MongoDB 数据库。现在,您将学习如何设置服务器。在您的 app.js 文件中使用以下代码。

  1. const connectToMongo = require("./db");
  2. const express = require("express");
  3. const app = express();
  4. connectToMongo();
  5. app.use(express.json());
  6. app.use("/auth", require("./auth"));
  7. const port = 3300;
  8. app.listen(port, () => {
  9. console.log(`Listening at http://localhost:${port}`);
  10. });

在这里,我们导入 expressdb.js 模块来连接到 MongoDB。然后,我们使用 express.json() 中间件来处理 JSON 响应。路由被创建在一个不同的模块 (auth.js) 中,以保持代码的清晰和组织良好。最后,我们在本地端口 3300 上创建一个服务器端点以进行监听。(您可以使用任何您选择的端口)

加密密码并将其存储在 MongoDB 数据库中

到目前为止,服务器几乎已准备就绪,现在您将为服务器创建端点。我们将创建两个端点 – signuplogin。在注册端点中,我们将从新用户那里获取电子邮件和密码,并使用 BcryptJS 进行密码加密后存储。

auth.js 文件中,输入以下代码:

  1. const express = require("express");
  2. const router = express.Router();
  3. const User = require("./User");
  4. const bcrypt = require("bcryptjs");
  5. // 路由 1:
  6. router.post("/signup", async (req, res) => {
  7. const salt = await bcrypt.genSalt(10);
  8. const secPass = await bcrypt.hash(req.body.password, salt);
  9. let user = await User.create({
  10. email: req.body.email,
  11. password: secPass,
  12. });
  13. res.json({ user });
  14. });
  15. module.exports = router;

我们进行必要的导入,然后设置 express 路由器 来创建 /signup 端点。我们使用 POST 方法,以便在应用程序的 URL 中不泄露凭据。之后,我们使用 scripts 包的 genSalt 函数创建一个盐;传递给 genSalt() 函数的参数包含盐字符的长度。然后,我们使用 BcryptJS 的 hash() 函数,它接受一个必需的参数,即要转换为哈希代码的密码字符串,以及一个可选参数,盐字符串。然后它返回一个包含 passwordsalt 的哈希值。

之后,我们使用mongoose模块的create()函数在数据库中根据users模型的规则创建一个文档。它接受一个包含电子邮件和密码的Javascript对象,但我们不直接提供原始字符串,而是提供secPass密码哈希 + )以存储在数据库中。这样,我们使用哈希结合盐来安全地存储密码,而不是使用原始字符串。最终,我们返回一个包含用户模型的JSON响应。(此发送响应的方法仅用于开发阶段;在生产中,您将使用身份验证令牌或其他内容替换此方法)。

要测试此端点,您必须首先运行服务器,可以通过在终端中键入以下命令来完成。

cd <path to your project folder>
nodemon ./app.js

此命令将在localhost和端口3300(或您指定的任何端口)上运行服务器。然后,您可以向URLhttp://localhost:3300/auth/signup发送HTTP请求,请求体为:

  1. {
  2. "email":"[email protected]",
  3. "password":"sammy"
  4. }

这将产生以下输出/响应:

{
  "user": {
    "email": "[email protected]",
    "password": "$2a$10$JBka/WyJD0ohkzyu5Wu.JeCqQm33UIx/1xqIeNJ1AQI9kYZ0Gr0IS",
    "_id": "654510cd8f1edaa59a8bb589",
    "__v": 0
  }
}

注意:密码哈希和ID将不同,因为它们始终是唯一的。

在接下来的部分中,您将学习如何访问存储的密码哈希,并使用登录/身份验证时提供的密码进行身份验证。

访问加密密码并将其用于身份验证

到目前为止,您已经了解了BcryptJS,散列,加盐,以及开发一个express服务器,该服务器将新用户的密码存储为哈希值。现在,您将学习如何使用存储的密码,并在用户尝试登录应用程序时对其进行身份验证。

要添加身份验证方法,请在您的auth.js文件中/signup路由之后添加以下路由:

  1. // 路由2:
  2. router.post("/login", async (req, res) => {
  3. let user = await User.findOne({ email: req.body.email });
  4. if (!user) {
  5. return res.status(400).json({ error: "Login with proper credentials!" });
  6. }
  7. const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  8. if (!passwordCompare) {
  9. return res
  10. .status(400)
  11. .json({ error: "Login with proper credentials!" });
  12. }
  13. res.json({ success: "Authenticated!" });
  14. });

在这里,我们使用/auth/login子路径来为已存在的用户执行登录身份验证。与/auth/signup端点类似,这将是一个异步等待函数,因为BcryptJS返回承诺。

首先,我们使用Mongoose库的findOne函数,该函数用于根据给定的搜索查询在集合中查找文档。在这种情况下,我们根据电子邮件搜索用户。如果不存在具有提供的电子邮件的用户,则会发送状态码为400的无效凭据响应。(在登录时不提供哪个参数不正确并不是一种好的做法,因为攻击者可以使用该信息来查找现有帐户)。

如果用户提供的电子邮件存在,则程序将移动到密码比较。为此,BcryptJS提供了compare()方法,它以原始字符串作为第一个参数,并以哈希(带有或不带有)作为第二个参数。然后,它返回一个布尔值承诺;如果密码与哈希匹配,则为true,如果不匹配,则为false。然后,您可以使用if语句添加一个简单的检查,并根据比较返回成功或错误。

最后,您将使用module.exportsexpress路由器导出给app.js作为起始点来用于路由。

要测试此路由,您可以向此URLhttp://localhost:3300/auth/login发送另一个HTTP响应,并将其正文设置为以下内容:

  1. {
  2. "email":"[email protected]",
  3. "password":"sammy"
  4. }

此请求将产生以下响应:

{
  "success": "Authenticated!"
}

最后,auth.js将如下所示:

  1. const express = require("express");
  2. const router = express.Router();
  3. const User = require("./User");
  4. const bcrypt = require("bcryptjs");
  5. // 路由1:
  6. router.post("/signup", async (req, res) => {
  7. const salt = await bcrypt.genSalt(10);
  8. const secPass = await bcrypt.hash(req.body.password, salt);
  9. let user = await User.create({
  10. email: req.body.email,
  11. password: secPass,
  12. });
  13. res.json({ user });
  14. });
  15. // 路由2:
  16. router.post("/login", async (req, res) => {
  17. let user = await User.findOne({ email: req.body.email });
  18. if (!user) {
  19. return res.status(400).json({ error: "Login with proper credentials!" });
  20. }
  21. const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  22. if (!passwordCompare) {
  23. return res
  24. .status(400)
  25. .json({ error: "Login with proper credentials!" });
  26. }
  27. res.json({ success: "Authenticated!" });
  28. });
  29. module.exports = router;

结论

本教程创建了一个服务器,以解释BcryptJS的用法,用于安全存储和访问数据库密码。您可以通过以下方式进一步操作:

  • 实现更多用于其他任务的路由,例如获取用户凭据,更新用户凭据等。

  • 实施身份验证令牌以作为响应发送,等等。

这将启动您处理密码安全的旅程;您始终可以通过不同的技术和技术添加更多安全性,以创建更安全、更强大的应用程序。

Source:
https://www.digitalocean.com/community/tutorials/how-to-handle-passwords-safely-with-bcryptsjs-in-javascript