Все о добавлении аутентификации пользователей и защите конечных точек.

Это пятая статья из этой серии.

Рекомендуется продолжить работу с этой статьей, если у вас уже есть рабочий API, созданный с использованием Node, Express и MongoDB. Если нет, позвольте мне рассказать вам о том, что мы уже сделали.

В первой статье рассказывается о цели серии и о приложении, которое мы создаем в процессе.

Во второй статье рассказывается, что и как настроить в среде разработки с оптимизацией нужных инструментов.

В третьей статье подробно рассказывается, как мы разрабатывали операции CRUD для нашего приложения.

Четвертая статья упрощает развертывание нашего приложения и базы данных на Heroku и mLab.

В этой статье мы добавим еще одну функцию в наше приложение. Итак, приступим уже к работе. На данный момент наше приложение может выполнять только операции Создать, Получить, Обновить и Удалить, нам нужно только дать пользователям возможность зарегистрироваться, а также войти в систему. Это довольно просто, но может оказаться немного сложнее. Итак, давайте сначала разберемся со сложностями, прежде чем писать какую-либо строчку кода.

Включение регистрации / входа пользователей в нашем приложении означает, что пользователям необходимо будет предоставить некоторые личные данные, например имя пользователя и пароль. Что произойдет, если два уникальных пользователя отправят одно и то же имя пользователя, объединит ли сервер их как одного? Или что происходит, когда наша база данных скомпрометирована, означает ли это, что все пароли наших пользователей останутся открытыми? Вот простое прочтение о безопасности паролей. Обеспечение того, чтобы пароли ваших пользователей не просто хешировались, а добавлялись соли, - лучший способ защитить их конфиденциальность. В этом приложении мы будем использовать bcrypt.

Это подводит нас к следующему вопросу: что происходит, когда пользователь предоставляет действительное имя пользователя и пароль? Что мы (сервер) предоставляем пользователю, чтобы он мог получить доступ к определенному защищенному маршруту? Как мы проверяем, что этот пользователь имеет право доступа к определенной информации? Как и в любом другом круге, определенные привилегии обычно закрепляются за уполномоченными лицами и ограничиваются общественным потреблением. Для этого воспользуемся JWT.

Вы готовы к кодам? Давай сделаем это. Если вы выполнили все статьи, по умолчанию вы должны быть в ветке master. Примечание: вы никогда не должны писать код в своей ветке master, вы знаете, что такое упражнение. Оформить заказ для разработки, а затем оформить заказ в новую ветку. Затем npm установите bcrypt и jsonwebtoken.

// checkout to develop branch
git checkout develop
// create a new branch and checkout to this
git checkout -b ft-userauth
// npm install 
npm install bcrypt jsonwebtoken --save

Установлены? Потрясающие. В нашей предыдущей статье мы обсуждали, какие коды идут в модель, контроллер и маршруты. Давайте поработаем над моделью пользователя.

Модель пользователя

Создайте новый файл user.js в каталоге модели. Использование встроенного терминала git.

// create user model file
touch server/models/user.js

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

Если изображение выше не загружается, это код ниже.

import mongoose form 'mongoose';
mongoose.Promise = global.Promise;
const userSchema = new mongoose.Schema({
  _id: mongoose.Schema.Types.ObjectId,
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowerCase: true,
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowerCase: true,
    match: [/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?      ^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/],
  },
  password: {
    type: String,
    required: true,
  },
});
export default mongoose.model('User', userSchema);

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

Пользовательский контроллер

Создайте новый файл user.js в каталоге контроллера. Использование встроенного терминала git.

// create user controller
touch server/controllers/user.js

Хорошо, здесь все становится сложно. Итак, давайте попробуем понять процесс, прежде чем писать какой-либо код. Сначала мы хотим получить req.body от клиента, затем мы хотим хешировать пароль, прежде чем он попадет в базу данных. Затем мы хотим проверить, отправил ли клиент / пользователь требуемые значения, а затем проверить, существует ли то, что было отправлено, в базе данных. Если он существует в базе данных, проинформируйте пользователя каким-либо ответом, иначе сохраните в базе данных. Теперь, пока он сохраняется, сгенерируйте уникальный токен для авторизации доступа к защищенным конечным точкам. Это позаботится о регистрации.

Для входа мы хотим получить req.body от клиента, а затем попытаться проверить предоставленный пароль. Теперь помните, что хеширование пароля делает его односторонней функцией, это означает, что мы не можем получить пароль из хеша, пароль должен совпадать с хешем, чтобы он работал. После того, как мы (сервер) проверили совпадение пароля, мы должны ответить токеном, который должен быть отправлен для доступа к защищенным маршрутам. О, я забыл упомянуть, что мы не будем создавать и проверять наш токен в контроллере. Нам нужно будет создать промежуточное программное обеспечение, чтобы справиться с этим. Вот и все. Напишем коды.

Конечная точка регистрации

Сначала создайте промежуточное ПО для генерации токена. Создайте файл token.js внутри каталога промежуточного программного обеспечения. Используйте встроенный терминал git для кодов ниже:

// create a middleware directory
mkdir server/middleware
// create a token.js
touch server/middleware/token.js
// open up the token.js file
start server/middleware/token.js

Token.js будет содержать уникальный ключ, который будет использоваться для случайной генерации токена авторизации. Теперь мы не хотим, чтобы этот токен оставался открытым, поэтому мы определяем его в файле .env. Вот как: добавьте строку кода ниже в .env файл. Пожалуйста, замените "privatekey" на произвольную строку, которая вам удобна.

KEY = privatekey

Добавьте следующий код в token.js

import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
dotenv.config();
const key = process.env.KEY;
const Token = ({ id }) => jwt.sign(
  { id },
  key,
  { expiresIn: '2h' },
);
export default Token;

Мы импортировали jwt, который является модулем, который будет создавать токен, затем присвоили ключу значение, которое мы объявили в .env, и с его помощью создали токен. Срок действия токена истекает через два часа. Не стесняйтесь настраивать это по своему усмотрению. Вот как".

// import required dependencies
import mongoose from 'mongoose';
import bcrypt from 'bcrypt';
import User from '../models/user';
import Token from '../middleware/token';
export function createUser(req, res) {
  bcrypt.hash(req.body.password, 15, (err, hash) => {
    const password = hash;
    const user = new User({
      _id: new mongoose.Types.ObjectId(),
      username: req.body.username,
      email: req.body.email,
      password,
    });
// check that user submits the required value
    if (!user.username || !user.email || !user.password) {
      return res.status(400).json({
        message: 'Please ensure you fill the username, email, and password',
      });
    }
// verify the user isn't stored in the database
    return User.count({
      $or: [
        { username: req.body.username },
        { email: req.body.email },
      ],
    })
    .then((count) => {
      (count > 0) {
        res.status(401).json({
          message: 'This user exists',
        });
      }
// if user doesn't exist, create one
      return user
        .save()
        .then((newUser) => {
          const token = Token(newUser);          
          res.status(201).json({
            message: 'User signup successfully',
            newUser,
            token,          
          });
        })
        .catch(() => {
          res.status(500).json({
            message: 'Our server is in the locker room, please do try again.'
          });
        });
      });
    }); 
}
export default User;

Было много кодов? Давай поработаем. Мы импортировали то, что нам нужно, включая токен, над которым мы работали в промежуточном программном обеспечении. Затем мы хешировали входящий пароль и добавляли несколько раундов соления, а затем назначали этот хеш паролю. Чтобы лучше понять, см. Здесь. Мы также проверили, что пользователь отправляет обязательные поля, а также убедились, что имя пользователя и адрес электронной почты не сохранены в базе данных. Если это так, мы должны отправить пользователю небольшой приятный ответ, иначе сохраните. Все должно пройти хорошо, но если это не так, мы должны отправить отличный (ИМХО) ответ клиенту.

Осталось еще кое-что, добавить конечную точку в каталог маршрута.

...
import { createUser } from '../controllers/user';
...
router.post('/user/signup', createUser);
...

Замечательно… Давайте проверим это с помощью Postman.

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

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

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

Давайте зарегистрируем другого пользователя…

Конечная точка входа

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

...
export function loginUser(req, res) {
  const { username, password } = req.body;
  User.findOne({ username })
    .then((existingUser) => {
      bcrypt.compare(password,existingUser.password, (err, result) {
        if (err) {
          return res.status(401).json({
            message: 'Not authorized',
          });
        }
        if (result) {
          const token = Token(existingUser);
          return res.status(200).json({
            message: 'User authorization successful',
            existingUser: {
              username: existingUser.username,
              email: existingUser.email,
              _id: existingUser.id,
            },
            token,
          });
        }
        return res.status(401).json({
          message: 'Invalid details',
        });
      });
    })
    .catch(() => res.status(500).json({ message: 'Our server is in the locker room, please do try again.' }));
}
...

Затем мы добавляем его в main.js в каталоге маршрутизатора.

...
import { createUser, loginUser } from '../controllers/user';
...
router.post('/user/login', loginUser);
...

Теперь, к последней части этой статьи ...

Защита конечных точек

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

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

Давайте приступим к этому прямо сейчас. Создайте файл verifytoken.js в каталоге промежуточного программного обеспечения. Добавьте следующие строки кода.

import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
dotenv.config();
module.exports = (req, res, next) => {
  const token = req.headers['x-access-token'];
  if (token) {
    jwt.verify(token, process.env.KEY, (err, decoded) => {
      if (err) {
        return res.status(401).json({
          message: 'Authorization failed',
        });
       } else {
         req.decoded = decoded;
         next();
       }
     });
   } else {
      return res.status(401).json({
        message: 'Authorization failed',
      });
   }
}

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

...
import verifyToken from '../middleware/verifytoken';
...
router.post('/causes', verifyToken, createCause);
...

И это все. На данный момент мы успешно разработали наш код, чтобы делать следующее:

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

Давайте сейчас это проверим, ладно?

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

Скопируйте токен и добавьте его в заголовок. Убедитесь, что ключ установлен на: x-access-token

Далее мы пытаемся создать причину. Икенна увлечена кормлением ребенка, не так ли? Не стесняйтесь реализовывать методы PATCH и DELETE. Сообщите мне, как это сработало для вас.

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

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

Спасибо, что прочитали это много. Были ли у вас проблемы с внедрением кодов? Давайте обсудим это в сеансе комментариев.

Еще раз спасибо за чтение. ПОЖАЛУЙСТА, ПОЖАЛУЙСТА, ПОДЕЛИТЬСЯ этой статьей с кем-нибудь.