Поиск по сайту:

Как безопасно обрабатывать пароли с помощью BcryptsJS в JavaScript


Автор выбрал Фонд OWASP для получения пожертвования в рамках программы Write for DOnations.

Введение

Защита паролей веб-сайтов — важный навык, которым должен обладать любой разработчик. JavaScript обеспечивает безопасное хранение и обработку паролей или других конфиденциальных данных с использованием алгоритмов хеширования, предоставляемых модулем BcryptJS JavaScript.

В этом руководстве вы узнаете о BcryptJS и хешировании для настройки базового экспресс-сервера, который будет хранить пароли в виде хешей в базе данных, а не необработанных строк, и извлекать их для аутентификации пароля.

Предварительные условия

Чтобы продолжить работу с этим руководством, у вас должны быть следующие настройки.

  1. На вашем компьютере установлена стабильная версия Node.js версии 12.x или выше. Вы можете использовать это руководство DigitalOcean для установки последней версии Node Js на свой компьютер.

  2. Вы должны знать, как программировать на JavaScript.

  3. _На вашем компьютере должен быть установлен Express JS. Вы можете использовать это руководство, чтобы узнать, как настроить сервер Express.

  4. Наконец, для выполнения этого руководства вам понадобится сообщество MongoDB или база данных Atlas. Вы можете установить его, используя одно из этих руководств DigitalOcean по установке MongoDB.

Зачем использовать BcryptJS?

Bcrypt — это алгоритм хэширования, позволяющий создавать хэши для паролей и хранить их в случае утечки данных. Этот усовершенствованный алгоритм хеширования использует соли, что затрудняет его взлом с помощью таких атак, как перебор.

BcryptJS — это JavaScript-реализация алгоритма хеширования Bcrypt, позволяющая использовать хэш-шифрование без необходимости использования сложных функций хеширования. Вот некоторые из причин, которые делают BcryptJS отличным выбором для защиты паролей:

  1. Безопасность – BcryptJS реализует алгоритм Bcrypt, медленный алгоритм (который хорошо подходит для хеширования) и требует интенсивных вычислительных мощностей. Это ставит перед злоумышленниками сложную задачу по взлому хеша пароля, обеспечивая безопасность паролей даже в случае утечки данных._

  2. Соль – BcryptJS генерирует случайные соли для паролей, чтобы обеспечить безопасность хранения (более подробно мы узнаем о хэшах и солях в следующем разделе). Соли усложняют хэш относительно слабого пароля, что затрудняет его расшифровку.

  3. Простота использования. BcryptJS предоставляет разработчикам JavaScript инструмент для шифрования паролей, не требуя глубокого понимания хеширования._

На следующем этапе мы кратко узнаем о хешах и солях.

Как работает хеширование?

Прежде чем использовать эти концепции в своих проектах, вы должны понять, как работают хеширование и засолка.

Хеширование

Хеширование — это преобразование простой строки или открытого текста в строку случайных символов (шифрование). Это позволяет безопасно хранить и/или передавать конфиденциальные данные. Хеширование включает в себя следующие ключевые этапы:

Ввод данных. Во-первых, сохраняются данные любого типа (двоичные, символьные, десятичные и т. д.) в виде обычного текста или строки.

Хеш-функция. Хеш-функция — это математический алгоритм, который принимает входные данные и преобразует их в набор символов или хеш-кодов. Хэш-функции являются детерминированными (выдают один и тот же результат для одного и того же ввода) и односторонними функциями (это означает, что практически невозможно реконструировать выходные данные хэш-функций, т. е. хешировать их входные данные).

Сопротивление столкновению. Это означает, что хэш-функция создается с учетом идеи устойчивости к столкновениям, т. е. два разных входа не могут иметь одинаковый выход (хеш-код).

Аутентификация. Хэш-функции являются детерминированными и создают один и тот же хэш для одних и тех же входных данных. Таким образом, при аутентификации пароля, хранящегося в виде хэша, основная идея заключается в том, что если пароль для аутентификации соответствует хешу, хранящемуся в базе данных, пароль правильный.

Соление

Поскольку хеширование существует уже несколько десятилетий, появились такие разработки, как радужные таблицы, которые содержат миллиарды записей данных, содержащих строки данных и соответствующие им хэши, основанные на различных алгоритмах хеширования.

Теперь рассмотрим ситуацию, когда пользователь, создающий учетную запись на вашем сайте, создает слабый пароль. Таким образом, в случае утечки данных злоумышленник может просмотреть хеши ваших пользователей и найти совпадение хэша с учетной записью пользователя со слабым паролем. Это может привести к катастрофическим последствиям для приложений с высоким уровнем безопасности. Чтобы такого не произошло, используют соли.

Соление — это дополнительный уровень безопасности, добавляемый к хешам путем добавления случайной строки символов в хеш пароля перед его сохранением в базе данных. Таким образом, даже если в результате взлома произойдет утечка данных, злоумышленнику будет сложно расшифровать хэш, содержащий соль. Рассмотрим следующий пример:

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

Как мы ясно видим, пароль, хранящийся с солью, с меньшей вероятностью будет взломан из-за детерминированной природы хэшей. Следовательно, если злоумышленник, например, найдет эту строку хеш+пароль в радужной таблице, он получит не настоящую строку пароля, а нечто совершенно другое.

Теперь вы готовы использовать BcryptJS и защищать свои пароли стандартным отраслевым способом.

Установка BcryptJS и других необходимых модулей

Теперь, когда вы знаете о хешировании и солении, вам остается взять компьютер и начать программировать. Структура проекта будет следующей:

Во-первых, мы начнем с создания проекта 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.

const mongoose = require("mongoose");
const mongoURI = "mongodb://127.0.0.1:27017/bcrypt_database";
const connectMongo = async () => {

  try {
    await mongoose.connect(mongoURI);
    console.log("Connected to MongoDB!");
  } catch (error) {
    console.error("Error connecting to MongoDB: ", error.message);
  }
};

module.exports = connectMongo;

Здесь вы импортируете пакет mongoose, который предоставляет API для подключения javascript к MongoDB. Кроме того, в этом случае URI подключения предназначен для локальной установки mongoDB, если вы используете облачную базу данных (например, Atlas), вам нужно только изменить URI на URI вашей конкретной базы данных.

Информация URI похож на URL-адрес сервера с той разницей, что URI может идентифицировать имя и идентичность ресурсов, а также их расположение в Интернете. Напротив, URL-адрес — это подмножество URI, способное выполнять только последнее.

Функция connectToMongo является асинхронной функцией, поскольку mongoose.connect возвращает обещание JavaScript. Эта функция даст соединение для успешного выполнения. В противном случае он вернет ошибку.

Наконец, мы используем module.exports для экспорта этой функции при каждом импорте модуля db.js.

Шаг 2 – Создание пользовательской схемы

Вам понадобится базовая схема для создания или аутентификации пользователей с помощью базы данных MongoDB. Если вы не знаете, что такое схема, вы можете использовать это отличное руководство по DO, чтобы понять и создать схему в MongoDB. Мы будем использовать только два поля для нашей схемы: email и пароль.

Используйте следующий код в своем модуле User.js.

const mongoose  = require("mongoose");
const UserSchema = new mongoose.Schema({
    email:{
        type:String,
        required:true,
        unique:true
    },
    password:{
        type:String,
        required:true
    }
});
module.exports = mongoose.model('user', UserSchema)

В этой схеме мы создаем два поля: email и пароль. Оба они являются обязательными полями и имеют строковый тип данных. Кроме того, email является уникальным полем, означающим, что один адрес электронной почты можно использовать только один раз для создания учетной записи на этом сервере.

Наконец, мы экспортируем модель с именем users. Модель представляет собой схему на стороне базы данных. Вы можете рассматривать схему как правило для определения модели, тогда как модель хранится как коллекция в базе данных MongoDB.

Вы можете преобразовать схему в модель, используя функцию model() библиотеки mongoose.

Шаг 3 – Настройка сервера в app.js

Выполнив предыдущие шаги, вы успешно создали модель и модуль для подключения к базе данных MongoDB. Теперь вы научитесь настраивать сервер. Используйте следующий код в своем файле app.js.

const connectToMongo = require("./db");
const express = require("express");
const app = express();
connectToMongo();
app.use(express.json());
app.use("/auth", require("./auth"));
const port = 3300;
app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`);
});

Здесь мы импортируем модули express и db.js для подключения к MongoDB. Затем мы используем промежуточное программное обеспечение express.json() для обработки ответов JSON. Маршруты создаются в другом модуле (auth.js), чтобы код был чистым и организованным. Затем, наконец, мы создаем конечную точку для прослушивания сервером порта 3300 на локальном хосте. (Вы можете использовать любой порт по вашему выбору)

Шифрование паролей и их хранение в базе данных MongoDB

К этому моменту сервер практически готов, и теперь вы будете создавать конечные точки для сервера. Мы создадим две конечные точки — регистрация и вход. В конечной точке регистрации мы возьмем адрес электронной почты и пароль от нового пользователя и сохраним их с шифрованием пароля с помощью BcryptJS.

В файле auth.js введите следующий код:

const express = require("express");
const router = express.Router();
const User = require("./User");
const bcrypt = require("bcryptjs");

// ROUTE 1:
router.post("/signup", async (req, res) => {
  const salt = await bcrypt.genSalt(10);
  const secPass = await bcrypt.hash(req.body.password, salt);
  let user = await User.create({
    email: req.body.email,
    password: secPass,
  });
  res.json({ user });
});
module.exports = router;

Мы делаем необходимый импорт, а затем настраиваем экспресс-маршрутизатор для создания конечной точки /signup. Мы используем метод POST, чтобы учетные данные не раскрывались в URL-адресе приложения. После этого мы создаем Salt с помощью функции genSalt пакета scripts; параметр, передаваемый функциям genSalt(), содержит длину символа соли. Затем мы используем функцию hash() BcryptJS, которая принимает обязательный параметр, строку пароля, которую нужно преобразовать в хеш-код, и необязательный аргумент — солевую строку. А затем он возвращает хеш, который содержит как пароль, так и salt.

После этого мы используем функцию create() модуля mongoose для создания документа в нашей базе данных, определенного правилами модели users. Он принимает объект Javascript, содержащий адрес электронной почты и пароль, но вместо того, чтобы передавать ему необработанную строку, мы передаем secPass (хэш пароля + salt) для хранения в базе данных. Таким образом, мы надежно сохранили пароль в базе данных, используя его хэш в сочетании с солью вместо необработанной строки. В конечном итоге мы возвращаем ответ JSON, содержащий модель пользователя. (Этот метод отправки ответов предназначен только для этапа разработки; в рабочей версии вы замените его токеном аутентификации или чем-то еще).

Чтобы протестировать эту конечную точку, вы должны сначала запустить сервер, что можно сделать, введя следующую команду в терминале.

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

Эта команда запустит ваш сервер на локальном хосте и порту 3300 (или любой другой порт, который вы укажете). Затем вы можете отправить запрос HTTP на URL-адрес http://localhost:3300/auth/signup со следующим телом:

{
  "email":"sammy@linux-console.net",
  "password":"sammy"
}

Это приведет к следующему выводу/ответу:

{
  "user": {
    "email": "sammy@linux-console.net",
    "password": "$2a$10$JBka/WyJD0ohkzyu5Wu.JeCqQm33UIx/1xqIeNJ1AQI9kYZ0Gr0IS",
    "_id": "654510cd8f1edaa59a8bb589",
    "__v": 0
  }
}

Примечание. Хэш пароля и идентификатор не будут одинаковыми, поскольку они всегда уникальны.

В следующем разделе вы узнаете, как получить доступ к сохраненному хешу пароля и аутентифицировать его с помощью пароля, указанного при входе в систему/аутентификации.

Доступ к зашифрованному паролю и его использование для аутентификации

На данный момент вы узнали о BcryptJS, хешировании, солении и разработке экспресс-сервера, который создает новых пользователей. с паролями, хранящимися в виде хэшей. Теперь вы узнаете, как использовать сохраненный пароль и аутентифицировать пользователя, когда он пытается войти в приложение.

Чтобы добавить метод аутентификации, добавьте следующий маршрут в ваш auth.js после маршрута /signup:

// ROUTE 2:
router.post("/login", async (req, res) => {
  let user = await User.findOne({ email: req.body.email });
  if (!user) {
    return res.status(400).json({ error: "Login with proper credentials!" });
  }
  
  const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  if (!passwordCompare) {
    return res
      .status(400)
      .json({ error: "Login with proper credentials!" });
  }
  
  res.json({ success: "Authenticated!" });
});

Здесь мы используем подпуть /auth/login для выполнения аутентификации входа в систему для уже существующего пользователя. Подобно конечной точке /auth/signup, это будет функция асинхронного ожидания, поскольку BcryptJS возвращает обещания.

Во-первых, мы используем функцию findOne библиотеки Mongoose, которая используется для поиска документа в коллекции на основе заданного поискового запроса. В данном случае мы ищем пользователя по адресу электронной почты. Если пользователя с указанным адресом электронной почты не существует, будет отправлен ответ с кодом состояния 400 для недействительных учетных данных. (Не рекомендуется сообщать, какой параметр неверен при входе в систему, поскольку злоумышленники могут использовать эту информацию для поиска существующих учетных записей).

Если пользователь с указанным адресом электронной почты существует, программа переходит к сравнению паролей. Для этой цели BcryptJS предоставляет метод compare(), который принимает необработанную строку в качестве первого аргумента и хэш (с < или без него).salt) для второго аргумента. Затем он возвращает Логическое обещание; true, если пароль соответствует хешу, и false, если нет. Затем вы можете добавить простую проверку с помощью оператора if и возвращать успех или ошибку на основе сравнения.

Наконец, вы экспортируете express router, используя module.exports в качестве начальной точки app.js, чтобы использовать его для маршрутов.

Чтобы протестировать этот маршрут, вы можете отправить еще один ответ HTTP на этот URL-адрес http://localhost:3300/auth/login с телом следующим образом:

{
  "email":"sammy@linux-console.net",
  "password":"sammy"
}

Этот запрос даст следующий ответ:

{
  "success": "Authenticated!"
}

В итоге auth.js будет выглядеть так:

const express = require("express");
const router = express.Router();
const User = require("./User");
const bcrypt = require("bcryptjs");

// ROUTE 1:
router.post("/signup", async (req, res) => {
  const salt = await bcrypt.genSalt(10);
  const secPass = await bcrypt.hash(req.body.password, salt);
  let user = await User.create({
    email: req.body.email,
    password: secPass,
  });
  res.json({ user });
});

// ROUTE 2:
router.post("/login", async (req, res) => {
  let user = await User.findOne({ email: req.body.email });
  if (!user) {
    return res.status(400).json({ error: "Login with proper credentials!" });
  }
  const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  if (!passwordCompare) {
    return res
      .status(400)
      .json({ error: "Login with proper credentials!" });
  }
  res.json({ success: "Authenticated!" });
});
module.exports = router;

Заключение

В этом руководстве создан сервер, на котором объясняется использование BcryptJS для безопасного хранения паролей базы данных и доступа к ним. Вы можете пойти дальше:

  • Реализация большего количества маршрутов для других задач, таких как получение учетных данных пользователя, обновление учетных данных пользователя и т. д.

  • Реализация токенов аутентификации для отправки в качестве ответов и т. д.

С этого начинается ваш путь к безопасной работе с паролями; вы всегда можете повысить безопасность с помощью различных методов и технологий, что позволит вам создавать более безопасные и отказоустойчивые приложения.

Статьи по данной тематике: