Обратите внимание, прежде чем мы начнем - это пошаговое руководство для простого приложения Express, в котором вы можете входить и выходить из системы. Я буду использовать Express для настройки сервера, Mongoose для работы с базой данных, мой собственный API для обработки логина пользователя, bcrypt для шифрования паролей и JWT для аутентификации пользователей.

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

Теперь, когда мы это прояснили, давайте продолжим и удачного кодирования!

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

npm init
npm i express mongoose body-parser cors bcrypt express-jwt jsonwebtoken validator morgan --save
npm i babel-cli babel-preset-es2015 browser-sync gulp gulp-babel gulp-clean-css gulp-nodemon gulp-sass gulp-uglify gulp-imagemin --save-dev

Что касается зависимостей разработчиков, я использую Babel, потому что я пишу ECMA6, но если это не так, вы можете опустить babel. Для запуска задач я использую Gulp.

Затем нам нужно создать индексные файлы вместе с gulpfile.

touch index.js index.html gulpfile.js

Заполните gulpfile.js этим кодом. Этот файл gulp позаботится о нашем сервере Node, SASS и ES6.

Теперь нам нужно создать файлы ES6 JS и SASS в папке src - мы будем писать код в этой папке (gulp скомпилирует эти файлы в общую папку), а также файлы ES5 JS и CSS в общей папке (которые будут обслуживать наш index. html и отобразить в браузере). (Боковое примечание: убедитесь, что вы редактируете файлы только в папке src. Если вы редактируете общую папку, а затем также вносите изменения в src, gulp изменит public, так что он будет таким же, как src, и вы потеряете все свои изменения. )

mkdir public src
mkdir public/js public/css src/js src/scss
touch public/js/app.js public/css/style.css src/js/app.js src/scss/style.scss

Теперь нам нужно настроить сервер и базу данных. Мы будем использовать Express для сервера и Mongoose для базы данных. Мы установим их в нашем index.js в корневом каталоге. Сначала нам нужно потребовать все пакеты, которые мы собираемся использовать с Express и Mongoose, а затем установить все промежуточное ПО.

const express    = require('express');
const morgan     = require('morgan');
const bodyParser = require('body-parser');
const cors       = require('cors');
const mongoose   = require('mongoose');
const port       = process.env.PORT || 3000;
const databaseUrl = 'mongodb://localhost/authentication-jwt';
const app = express();
mongoose.connect(databaseUrl);
app.use(morgan('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
app.use(express.static(`${__dirname}/public`));
app.use(express.static(`${__dirname}/bower_components`)); 
// for now you can comment out the line above 
// we're gonna install bower later on
app.listen(port, () => console.log(`Express running on port ${port}`));

Если мы попытаемся запустить приложение на этом этапе (используя команду gulp в нашем терминале), мы получим сообщение об ошибке: «Cannot GET /», которое связано с тот факт, что мы еще не установили маршрут для нашей домашней страницы. Итак, нам нужно создать две папки в нашем корневом каталоге: config - который будет содержать наш маршрутизатор, и контроллеры - содержащий логику маршрутизаторов.

mkdir config controllers
touch config/routes.js 
touch controllers/statics.js

Нам нужно сделать index.html нашей домашней страницей в statics.js.

module.exports = {
  home: staticsHome
};
const path = require('path');
function staticsHome(req, res){
  return res.sendFile(path.join(__dirname, '../index.html'));
}

И нам нужно установить обработчик маршрута для нашей домашней страницы в routes.js.

const express = require('express');
const router = express.Router();
const statics = require('../controllers/statics');
router.route('/').get(statics.home);
module.exports = router;

Теперь нам нужно потребовать маршрутизатор и использовать его в index.js. Перед нашим методом app.listen необходимо вставить следующий код.

const router = require('./config/routes');
app.use(router);

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

Я буду использовать Materialize для этого проекта и установлю его с помощью Bower. (Вы также должны установить express.static для bower_components в index.js - мы писали этот код в комментарии ранее.)

bower init
bower install materialize 

Я добавил панель навигации Materialize в index.html, добавил background: #fafafa в тело нашего style.scss и console.log ('работает') в app.js в наша папка src / js. Последнее, что нам нужно сделать перед тестированием, - это связать всю нашу таблицу стилей и скрипты в заголовке index.html.

<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/materialize/dist/css/materialize.min.css">
<script src="/jquery/dist/jquery.min.js" charset="utf-8"></script>
<script src="/materialize/dist/js/materialize.min.js" charset="utf-8"></script>
<script src="/js/app.js" charset="utf-8"></script>

Теперь, если вы запустите gulp в терминале, вы должны получить что-то вроде этого:

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

Мы должны установить прослушиватель событий для щелчков по нашей ссылке регистрации и входа в систему. Мы можем добавить к ним класс регистрации и входа, а все остальное обработать в app.js в папке src / js. Полные функции с формами html вы можете найти здесь.

function init() {
  $('.registration').on('click', registerForm);
  $('.login').on('click', loginForm);
}
function registerForm(e) {
  e.preventDefault();
  $('.container').html(` ADD YOUR FORM HTML HERE `);
}
function loginForm(e) {
  e.preventDefault();
  $('.container').html(` ADD YOUR FORM HTML HERE `);
}
$(init);

Теперь, когда вы нажали «Войти» или «Зарегистрироваться», вы должны увидеть форму. Следующим шагом является создание пользователя, и для этого нам нужно создать новую модель пользователя. Нам понадобится новая папка с именем models и файл user.js, где мы определим новую схему мангуста. Итак, models / user.js будет выглядеть так (часть I):

const mongoose = requier('mongoose');
const bcrypt = require('bcrypt');
const validator = require('validator');
const userSchema = new mongoose.Schema({
  username: { type: String, unique: true, required: true },
  email: { type: String, unique: true, required: true },
  passwordHash: { type: String, unique: true, required: true }
});
userSchema.virtual('password').set(setPassword);
userSchema.virtual('passwordConfirmation').set(setPasswordConfirmation);
function setPassword(password) {
  this._password = password;
  this.passwordHash = bcrypt.hashSync(password, bcrypt.genSaltSync(8));
}
function setPasswordConfirmation(passwordConfirmation) {
  this._passwordConfirmation = passwordConfirmation;
}

Для первой части нам нужно создать новый объект схемы, который будет обрабатывать новые записи в базе данных. Я назвал свою userSchema и установил для нее требуемые свойства имени пользователя, электронной почты и passwordHash, которые будут хранить зашифрованный пароль, который пользователь предоставляет во время регистрации. Мы собираемся получить пароль пользователя с помощью метода .virtual () и установить его с помощью функции setPassword, которая принимает один аргумент -password- encrypts он использует пакет bcrypt и сохраняет его в passwordHash. Мы также временно храним _passwordConfirmation, чтобы позже сравнить его с _password. _password и _passwordConfirmation не будут сохранены как только новый пользователь создается из-за того, как работает .virtual () - он только прослушивает свойства во время регистрации и удаляет их при сохранении нового пользователя.

userSchema.path('passwordHash').validate(validatePasswordHash);
function validatePasswordHash() {
  if (this.isNew) {
    if (!this._password) {
      return this.invalidate('password', 'A password is required.');
    }
    if (this._password.length < 6) {
      return this.invalidate('password', 'Password must be at least 6 characters.');
    }
    if (this._password !== this._passwordConfirmation) {
      return this.invalidate('password', 'Passwords do not match.');
    }
  }
}

Еще одна функция, которую мы должны добавить, - это validatePasswordHash, которая проверяет ввод пароля и делает недействительным всего пользователя, если пароли этого не делают: а) пароль вообще не вводится, б) пароль меньше 6 символов, в) пароль и поле подтверждения пароля не совпадают. Invalidates прерывает процесс сохранения, и любая попытка сохранить этого пользователя недействительна.

Мы также можем добавить проверку электронной почты:

userSchema.path('email').validate(validateEmail);
function validateEmail(email) {
  if (!validator.isEmail(email)) {
    return this.invalidate('email', 'A valid e-mail address is required.');
  }
}

Наконец, нам нужно добавить еще один метод в userSchema-one для проверки пароля для входа в систему, который будет использоваться позже, и module.exports. И это все, что касается пользовательской модели.

userSchema.methods.validatePassword = validatePassword;
function validatePassword(password) {
  return bcrypt.compareSync(password, this.passwordHash);
}
module.exports = mongoose.model('User', userSchema);

Теперь, когда у нас есть формы и модель пользователя, следующим шагом будет обработка регистрации и входа пользователей. Когда кто-то регистрируется, нам нужно создать новую запись в базе данных с его данными. На этом этапе мы также можем сгенерировать токен JWT для последующей аутентификации. Токен JWT - это строка, которая хранится в локальном хранилище браузера и позволяет пользователям получать доступ к странице без необходимости повторного входа в систему. Подумайте, как вы можете открыть Twitter и вам не придется входить в систему, если вы не вышли из системы во время последнего сеанса. Если вы хотите узнать больше о JWT, их сайт - хорошая отправная точка. Создайте Authentication.js в папке controllers, которая будет содержать этот код:

const User = require('../models/user');
const jwt = require('jsonwebtoken');
const secret = 'Something top secret.';
function authenticationsRegister(req, res){
  User.create(req.body.user, (err, user) => {
    if (err) return res.status(500).json({message: 'Something went wrong.'});
    const token = jwt.sign(user._id, secret, { expiresIn: 60*60*24 });
    return res.status(201).json({
      message: `Welcome ${user.username}!`,
      user,
      token
    });
  });
}

Как всегда, нам сначала нужно запросить пакеты, которые мы собираемся использовать. Затем я определил функцию под названием AuthenticationRegister, которая обрабатывает новых пользователей. User.create () примет форму и создаст нового пользователя на основе отправленной формы (содержащейся в теле запроса). Если произойдет какая-либо ошибка, мы отправим код состояния 500 и сообщение «Что-то пошло не так». Переменная токена сгенерирует новый токен для этого пользователя. Функция jwt.sign принимает три аргумента: user._id, который является идентификатором, который пользователь назначил ему в базе данных (мангуст делает это автоматически), secret - это строка. который мы определили ранее в файле, это случайная строка, которая помогает создавать и кодировать уникальный токен для каждого пользователя, expiresIn не требует пояснений - этот токен действителен в течение 24 часов. Когда пользователь создается, мы отправляем обратно код состояния 201 (создан) и json, содержащий сообщение, объект пользователя и токен jwt.

Теперь нам нужно обработать логин уже существующего пользователя.

function authenticationsLogin(req, res) {
  User.findOne( {email: req.body.email}, (err, user) => {
    if (err) return res.status(500).json({ message: 'Something went wrong.'});
    if (!user || !user.validatePassword(req.body.password)) {
      return res.status(401).json({ message: 'Unauthorised.'});
    }
    const token = jwt.sign(user._id, secret, { expiresIn: 60*60*24});
    return res.status(200).json({
      message: 'Welcome back',
      user,
      token
    });
  });
}
module.exports = {
  register: authenticationsRegister,
  login: authenticationsLogin
};

Когда пользователь отправляет форму входа в систему, функция authenticationLogin будет искать этого пользователя в базе данных на основе предоставленного адреса электронной почты. Если пользователь не найден или введен неправильный пароль, функция возвращает код состояния 401 (неавторизованный). Мы проверяем пароль с помощью функции validatePassword (определенной в модели пользователя), которая возвращает истину или ложь, если пароль совпадает с passwordHash или нет (метод compareSync bcrypt сравнивает строку пароля со строкой passwordHash). Наконец, если все прошло успешно, мы возвращаем код состояния 200 и json с сообщением, пользователем и токеном jwt.

Теперь, когда мы определили функции аутентификации, нам нужно создать для них маршруты. Создайте apiRoutes.js в папке config.

const express = require('express');
const router = express.Router();
const authentication = require('../controllers/authentications');
router.route('/register')
  .post(authentication.register);
router.route('/login')
  .post(authentication.login);
module.exports = router;

Прежде чем мы сможем проверить, работают ли наши маршруты API, нам нужно добавить несколько строк кода в index.js.

const apiRouter   = require('./config/apiRoutes');
const expressJWT = require('express-jwt');
const secret = 'Something top secret.';
app.use('/api', expressJWT({ secret })
  .unless({
    path: [
      { url: '/api/register', methods: ['POST'] },
      { url: '/api/login',    methods: ['POST'] }
    ]
  }));
app.use(jwtErrorHandler);
function jwtErrorHandler(err, req, res, next){
  if (err.name !== 'UnauthorizedError') return next();
  return res.status(401).json({ message: 'Unauthorized request.' });
}
app.use('/api', apiRouter);

Я использую Insomnia для тестирования API, поэтому, как только я сохраню весь код, я могу открыть его и создать нового пользователя, чтобы посмотреть, все ли работает как надо:

Я протестировал маршрут «/ api / register», и, как вы можете видеть, API вернул сообщение, объект пользователя и токен так же, как мы настроили ранее. Мы также можем проверить токен, и если он работает, перейдя в заголовок и добавив Authorization в Content-type, а в application / json мы добавим Bearer, за которым следует значение токена без цитаты вроде этого: (примечание: это авторизация, а не аутентификация, как на картинке - мои извинения)

Теперь, если мы перейдем на главную страницу и изменим URL-адрес на «/ api / login» и введем наши данные для входа, мы увидим, что он вернул нам сообщение с приветствием.

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

(В этот момент я подумал, что заслужил перерыв, и несколько часов играл в Тени Мордора, убил несколько сотен уруков, и теперь я готов продолжить. Так что, если вы хотите сделать паузу и пообедать, я не буду судить. Нет? Хорошо, давайте продолжим!)

const apiUrl = 'http://localhost:3000/api';
function init() {
  $('.registration').on('click', registerForm);
  $('.login').on('click', loginForm);
  $('body').on('submit', 'form', submittedForm);
}
function submittedForm(e) {
  if (e) e.preventDefault();
  $.ajax({
    url: `${apiUrl}${$(this).attr('action')}`,
    method: $(this).attr('method'),
    data: $(this).serialize(),
    beforeSend: setRequestHeader
  }).done((data) => {
    if (data.token) setToken(data.token);
  });
}
function setToken(token) {
  return window.localStorage.setItem('token', token);
}
function setRequestHeader(xhr) {
  return xhr.setRequestHeader('Authorization', `Bearer ${getToken()}`);
}
function getToken() {
  return window.localStorage.getItem('token');
}

Слушатель отправки необходим для форм с функцией обратного вызова submitForm. Мы не можем добавить прослушиватель событий в форму напрямую, поскольку они не были частью DOM, когда страница была загружена, и обратный вызов не будет выполняться при отправке формы. Делегирование событий может справиться с этим - сначала мы добавляем прослушиватель событий к родительскому элементу, который изначально был частью DOM - в данном случае body - затем мы добавляем функцию .on () и вызываем событие - 'submit' - второй аргумент - это элемент, к которому мы хотим привязать слушателя - 'form', а последний аргумент - это функция обратного вызова.

Внутри функции SubmittedForm находится запрос AJAX. Мы отправляем данные формы в базу данных методом post. Частью запроса AJAX является функция setRequestHeader beforeSend, которая добавляет токен jwt в заголовок запроса. Функция обратного вызова для запроса AJAX - это функция setToken, которая добавляет токен jwt в локальное хранилище браузера.

Последняя часть приложения предназначена для обработки выхода и внешнего вида страницы в зависимости от того, вошли вы в систему или вышли из нее. Нам нужно добавить класс «loggedIn» к навигационной ссылке .logout и класс «loggedOut» к навигационным ссылкам .register и .login. Затем мы добавляем прослушиватель кликов в .logout и записываем этот код в app.js:

function removeToken() {
  return window.localStorage.clear();
}
function loggedInState() {
  $('.loggedIn').show();
  $('.loggedOut').hide();
}
function loggedOutState() {
  $('.loggedIn').hide();
  $('.loggedOut').show();
}
function logOut(e) {
  e.preventDefault();
  loggedOutState();
  loginForm();
  removeToken();
}

Наша функция init теперь должна выглядеть так:

function init() {
  $('.registration').on('click', registerForm);
  $('.login').on('click', loginForm);
  $('.logout').on('click', logOut);
  $('body').on('submit', 'form', submittedForm);
  if (getToken()) {
    loggedInState();
  } else {
    loggedOutState();
  }
}

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

Надеюсь, вы нашли это руководство полезным, и если у вас есть вопросы, не стесняйтесь их задавать!

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

До следующего раза и счастливого кодирования!