javascript

Веб-приложение на Node и Vue, часть 1: структура проекта, API, аутентификация

  • вторник, 24 октября 2017 г. в 03:13:31
https://habrahabr.ru/company/ruvds/blog/340750/
  • Разработка веб-сайтов
  • Node.JS
  • MongoDB
  • JavaScript
  • Блог компании RUVDS.com


Перед вами — первый материал из серии, посвящённой разработке полноценного веб-приложения, которое называется Budget Manager. Основные программные средства, которые будут использованы в ходе работы над ним — это Node.js для сервера, Vue.js для фронтенда, и MongoDB в роли базы данных.



Эти материалы рассчитаны на читателей, которые знакомы с JavaScript, имеют общее представление о Node.js, npm и MongoDB, и хотят изучить связку Node-Vue-MongoDB и сопутствующие технологии. Приложение будем писать с нуля, поэтому запаситесь любимым редактором кода. Для того, чтобы не усложнять проект, мы не будем пользоваться Vuex и постараемся сосредоточиться на самом главном, не отвлекаясь на второстепенные вещи.

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

Здесь мы рассмотрим следующие вопросы:

  • Организация структуры проекта.
  • Установка зависимостей и описание используемых библиотек.
  • Работа с MongoDB и создание моделей Mongoose.
  • Разработка методов API приложения.
  • Подготовка маршрутов Express.
  • Организация JWT-аутентификации с применением Passport.js.
  • Тестирование проекта с использованием Postman.

Код проекта, над которым мы будем работать, можно найти на GitHub.

Структура проекта и установка зависимостей


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


Структура папок API

В ходе продвижения по материалу мы значительно расширим эту структуру.

Теперь нужно установить несколько зависимостей. Для этого перейдём в корневую папку проекта (здесь это focus-budget-manager) и, предварительно сформировав package.json командой npm init, выполним следующую команду:

npm i --save express body-parser mongoose consign cors bcrypt jsonwebtoken morgan passport passport-jwt module-alias

Рассмотрим некоторые из этих зависимостей и их роль в проекте:

  • Express. Это — фреймворк для Node.js, мы будем пользоваться им для того, чтобы облегчить разработку API.
  • Body Parser. Этот пакет является средством разбора тела запросов для Node.js. Он помогает парсить тела входящих запросов до того, как они попадут в обработчики, в результате, работать с ними можно, используя свойство req.body.
  • Mongoose. Это — средство объектного моделирования для MongoDB, которое предназначено для работы в асинхронной среде.
  • Consign. Этот пакет является вспомогательным средством, использовать его не обязательно. Он предназначен для организации автозагрузки скриптов.
  • CORS. Этот пакет является вспомогательным средством для Connect/Express, которое можно использовать для активации CORS.
  • Bcrypt. С помощью этого пакета мы будем генерировать криптографическую «соль» и хэши.
  • Morgan. Это — вспомогательное средство для Node.js, предназначенное для логирования HTTP-запросов.
  • Module Alias. Этот пакет позволяет создавать псевдонимы для папок и регистрировать собственные пути к модулям в Node.js.

После установки пакетов, если вы планируете использовать Git, создайте в корневой папке проекта файл .gitignore. Запишите в него следующее:

/node_modules/

Теперь, когда предварительная подготовка завершена, займёмся программированием.

Файл BudgetManagerAPI/config/index.js


Создадим в папке BudgetManagerAPI/config файл index.js и внесём в него следующий код:

module.exports = {
  secret: 'budgetsecret',
  session: { session: false },
  database: 'mongodb://127.0.0.1:27017/budgetmanager'
}

Этот файл содержит параметры подключения к базе данных и секретный ключ, который мы используем для создания JWT-токенов.

Здесь предполагается работа с локальным сервером MongoDB. При этом в строке 127.0.0.1:27017 можно использовать localhost. Если хотите, можете работать с облачной базой данных MongoDB, созданной, например, средствами MLabs.

Файл BudgetManagerAPI/app/models/user.js


Создадим модель User, которая будет использоваться для JWT-аутентификации. Для этого надо перейти в папку BudgetManagerAPI/app и создать в ней директорию models, а в ней — файл user.js. В начале файла подключим зависимости:

const mongoose = require('mongoose'),
      bcrypt = require('bcrypt');

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

После этого, в тот же файл, добавим следующее:

const Schema = mongoose.Schema({
  username: {
    type: String,
    unique: true,
    required: true
  },
  password: {
    type: String,
    required: true
  },
  clients: [{}]
});

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

  • Имя пользователя (username).
  • Пароль (password).
  • Список клиентов (clients).

В сведения о клиенте будут входить адрес электронной почты (email), имя (name), телефон (phone), и финансовые документы (budgets). Финансовый документ включает такие данные, как его состояние (state), заголовок (title), элементы (items) и цена (price).

Продолжаем работать с файлом user.js, добавляем в него следующий код:

// Здесь не будем пользоваться стрелочными функциями из-за автоматической привязки к лексической области видимости
Schema.pre('save', function (next) {
  const user = this;
  if (this.isModified('password') || this.isNew) {
    bcrypt.genSalt(10, (error, salt) => {
    if (error) return next(error);
    bcrypt.hash(user.password, salt, (error, hash) => {
      if (error) return next(error);
      user.password = hash;
        next();
      });
    });
  } else {
    return next();
  }
});

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

Schema.methods.comparePassword = function (password, callback) {
  bcrypt.compare(password, this.password, (error, matches) => {
    if (error) return callback(error);
    callback(null, matches);
  });
};

Теперь, в конце файла, создадим модель User:

mongoose.model('User', Schema);

Файл BudgetManagerAPI/config/passport.js


После того, как модель User готова, создадим файл passport.js в папке BudgetManagerAPI/config. Начнём работу над этим файлом с подключения зависимостей:

const PassportJWT = require('passport-jwt'),
      ExtractJWT = PassportJWT.ExtractJwt,
      Strategy = PassportJWT.Strategy,
      config = require('./index.js'),
      models = require('@BudgetManager/app/setup');

Пакет mongoose нужен для работы с моделью User, а passport-jwt — для организация аутентификации.

Теперь добавим в этот файл следующее:

module.exports = (passport) => {
  const User = models.User;
  const parameters = {
    secretOrKey: config.secret,
    jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken()
  };
  passport.use(new Strategy(parameters, (payload, done) => {
    User.findOne({ id: payload.id }, (error, user) => {
      if (error) return done(error, false);
      if (user) done(null, user);
      else done(null, false);
    });
  }));
}

Тут мы создаём экземпляр модели User и находим пользователя, выполняя поиск по JWT-токену, полученному от клиента.

Файл BudgetManagerAPI/config/database.js


В папке BudgetManagerAPI/config создадим файл database.js, который ответственен за работу с базой данных. Добавим в этот файл следующее:

module.exports = (mongoose, config) => {
  const database = mongoose.connection;
  mongoose.Promise = Promise;
  mongoose.connect(config.database, {
    useMongoClient: true,
    promiseLibrary: global.Promise
  });
  database.on('error', error => console.log(`Connection to BudgetManager database failed: ${error}`));
  database.on('connected', () => console.log('Connected to BudgetManager database'));
  database.on('disconnected', () => console.log('Disconnected from BudgetManager database'));
  process.on('SIGINT', () => {
    database.close(() => {
      console.log('BudgetManager terminated, connection closed');
      process.exit(0);
    })
  });
};

Тут мы сначала переключили mongoose на использование стандартного объекта Promise. Если этого не сделать, можно столкнуться с предупреждениями, выводимыми в консоль. Затем мы создали стандартное подключение mongoose.

Настройка сервера, файл services/index.js


После того, как мы справились с некоторыми из вспомогательных подсистем, займёмся настройкой сервера. Перейдите в папку services и откройте уже имеющийся в ней файл index.js. Добавьте в него следующее:

require('module-alias/register');
const http = require('http'),
      BudgetManagerAPI = require('@BudgetManagerAPI'),
      BudgetManagerServer = http.Server(BudgetManagerAPI),
      BudgetManagerPORT = process.env.PORT || 3001,
      LOCAL = '0.0.0.0';
BudgetManagerServer.listen(BudgetManagerPORT, LOCAL, () => console.log(`BudgetManagerAPI running on ${BudgetManagerPORT}`));

Мы начинаем с подключения module_alias, который мы настроим позже (шаг это необязателен, но такой подход поможет сделать код чище). Если вы решите не использовать пакет module_alias, то вместо @BudgetManagerAPI надо будет писать ./services/BudgetManagerAPI/config.

Для того чтобы запустить сервер, надо перейти в корневую директорию проекта и ввести команду node services в используемом вами интерпретаторе командной строки.

Файл BudgetManagerAPI/config/app.js


В директории BudgetManagerAPI/config создадим файл app.js. Для начала подключим зависимости:

const express = require('express'),
      app = express(),
      bodyParser = require('body-parser'),
      mongoose = require('mongoose'),
      morgan = require('morgan'),
      consign = require('consign'),
      cors = require('cors'),
      passport = require('passport'),
      passportConfig = require('./passport')(passport),
      jwt = require('jsonwebtoken'),
      config = require('./index.js'),
      database = require('./database')(mongoose, config);

В строке passportConfig = require('./passport')(passport) мы импортируем конфигурационный файл для passport, передавая passport в качестве аргумента, так как в passport.js имеется такая команда:



Благодаря такому подходу мы можем работать с passport внутри файла passport.js без необходимости подключать его.

Далее, в файле app.js, начинаем работу с пакетами и устанавливаем секретный ключ:

app.use(express.static('.'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(morgan('dev'));
app.use(cors());
app.use(passport.initialize());

app.set('budgetsecret', config.secret);

Как вариант, вместо использования пакета cors, можно сделать следующее:

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});

Настройка package.json


Перейдём в корневую директорию проекта, откроем package.json и добавим в него, сразу перед блоком dependencies, следующее:

"homepage": "https://github.com/gdomaradzki/focus-gestor-orcamentos#readme",
"_moduleAliases": {
    "@root": ".",
    "@BudgetManager": "./services/BudgetManagerAPI",
    "@BudgetManagerModels":"./services/BudgetManagerAPI/app/models",
    "@BudgetManagerAPI":"./services/BudgetManagerAPI/config/app.js",
    "@config": "./services/BudgetManagerAPI/config/index.js"
  },
"dependencies": {

Обратите внимание на то, что блок dependencies уже имеется в файле, поэтому нужно добавить туда лишь блоки homepage и _moduleAliases.

Благодаря этим изменениям можно будет обращаться к корневой директории проекта с помощью псевдонима @root, к конфигурационному файлу index.js — используя псевдоним @config, и так далее.

Файл BudgetManagerAPI/app/setup/index.js


После настройки псевдонимов перейдём в папку BudgetManagerAPI/app и создадим новую папку setup, а в ней — файл index.js. Добавим в него следующее:

const mongoose = require('mongoose'),
      UserModel = require('@BudgetManagerModels');
const models = {
  User: mongoose.model('User')
}
module.exports = models;

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

Файл BudgetManagerAPI/app/api/auth.js


Теперь начинаем создавать некоторые из методов API. Перейдём в папку BudgetManagerAPI/app, создадим в ней директорию api, а в ней — файл auth.js. Запишем в него следующее:

const mongoose = require('mongoose'),
      jwt = require('jsonwebtoken'),
      config = require('@config');

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

config = require('./../../config);

Теперь, после подключения пакетов, сделаем в том же файле следующее:

const api = {};

api.login = (User) => (req, res) => {
  User.findOne({ username: req.body.username }, (error, user) => {
    if (error) throw error;

    if (!user) res.status(401).send({ success: false, message: 'Authentication failed. User not found.' });
    else {
      user.comparePassword(req.body.password, (error, matches) => {
        if (matches && !error) {
          const token = jwt.sign({ user }, config.secret);
          res.json({ success: true, message: 'Token granted', token });
        } else {
          res.status(401).send({ success: false, message: 'Authentication failed. Wrong password.' });
        }
      });
    }
  });
}

Тут мы создаём пустой объект api, в котором сохраним все необходимые методы. В метод login сначала передаём аргумент User, так как тут нужен метод для доступа к модели User, затем передаём аргументы req и res.

Этот метод выполняет поиск объекта User, который соответствует имени пользователя (username). Если имя пользователя распознать не удаётся, выдаём ошибку, в противном случае проверяем пароль и токен, привязанные к пользователю.

Теперь нужен ещё один метод api, который будет получать и парсить токен:

api.verify = (headers) => {
  if (headers && headers.authorization) {
    const split = headers.authorization.split(' ');
  if (split.length === 2) return split[1];
    else return null;
  } else return null;
}

Этот метод проверяет заголовки и получает заголовок Authorization. После всех этих действий мы, наконец, можем экспортировать объект api:

module.exports = api;

Маршруты API, файл BudgetManagerAPI/app/routes/auth.js


Займёмся созданием маршрутов API. Для этого перейдём в папку services/BudgetManagerAPI/app и создадим в ней директорию routes, в которой создадим файл auth.js со следующим содержимым:

const models = require('@BudgetManager/app/setup');
module.exports = (app) => {
  const api = app.BudgetManagerAPI.app.api.auth;
  app.route('/')
     .get((req, res) => res.send('Budget Manager API'));
  app.route('/api/v1/auth')
     .post(api.login(models.User));
}

В этот модуль мы передаём объект app, благодаря чему можно установить маршруты. Здесь же мы задаём константу api, которую используем для работы с файлом auth.js в папке api. Тут мы задаём маршрут по умолчанию, '/', при обращении к которому пользователю передаётся строка «Budget Manager API». Тут же создаём маршрут '/api/v1/auth' (для работы с которым применяется POST-запрос). Для обслуживания этого маршрута используем метод login, передавая модель User как аргумент.

Файл BudgetManagerAPI/config/app.js


Вернёмся теперь к файлу app.js, который расположен в папке BudgetManagerAPI/config и добавим в него следующее (строка app.set('budgetsecret', config.secret) дана как ориентир, добавлять её в файл второй раз не надо):

app.set('budgetsecret', config.secret);
consign({ cwd: 'services' })
      .include('BudgetManagerAPI/app/setup')
      .then('BudgetManagerAPI/app/api')
      .then('BudgetManagerAPI/app/routes')
      .into(app);
module.exports = app;

Тут мы проверяем, прежде чем выполнять другие действия, загружено ли содержимое папки setup, благодаря чему в первую очередь будет создан экземпляр модели. Затем загружаем методы API, и наконец — маршруты.

Файл BudgetManagerAPI/app/api/user.js


Вернёмся в папку BudgetManagerAPI/app/api и создадим в ней файл user.js. Поместим в него следующий код:

const mongoose = require('mongoose');
const api = {};
api.setup = (User) => (req, res) => {
  const admin = new User({
    username: 'admin',
    password: 'admin',
    clients: []
  });
admin.save(error => {
    if (error) throw error;
console.log('Admin account was succesfully set up');
    res.json({ success: true });
  })
}

Метод setup позволяет создать учётную запись администратора, нужную для отладочных целей. В готовом приложении этой учётной записи быть не должно.

Теперь, в том же файле, создадим метод, применяемый для тестовых целей, позволяющий вывести список всех пользователей, которые зарегистрировались в приложении, и нужный для проверки механизмов аутентификации:

api.index = (User, BudgetToken) => (req, res) => {
  const token = BudgetToken;
if (token) {
    User.find({}, (error, users) => {
      if (error) throw error;
      res.status(200).json(users);
    });
  } else return res.status(403).send({ success: false, message: 'Unauthorized' });
}

Далее, создадим метод signup, который понадобится позже. Он предназначен для регистрации новых пользователей:

api.signup = (User) => (req, res) => {
  if (!req.body.username || !req.body.password) res.json({ success: false, message: 'Please, pass a username and password.' });
  else {
    const newUser = new User({
      username: req.body.username,
      password: req.body.password,
      clients: []
    });
    newUser.save((error) => {
      if (error) return res.status(400).json({ success: false, message:  'Username already exists.' });
      res.json({ success: true, message: 'Account created successfully' });
    })
  }
}
module.exports = api;

Тут проверяется, при попытке регистрации нового пользователя, заполнены ли поля username и password, а если это так, то, при условии, что введено допустимое имя пользователя, создаётся новый пользователь.

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

Файл BudgetManagerAPI/app/routes/user.js


Теперь создадим файл user.js в папке BudgetManagerAPI/app/routes и запишем в него следующий код:

const passport = require('passport'),
      config = require('@config'),
      models = require('@BudgetManager/app/setup');
module.exports = (app) => {
  const api = app.BudgetManagerAPI.app.api.user;
  app.route('/api/v1/setup')
     .post(api.setup(models.User))
  app.route('/api/v1/users')
     .get(passport.authenticate('jwt', config.session),  api.index(models.User, app.get('budgetsecret')));
  app.route('/api/v1/signup')
     .post(api.signup(models.User));
}

Здесь мы импортируем библиотеку passport для организации аутентификации, подключаем конфигурационный файл для настройки параметров сессии, и подключаем модели, благодаря чему можно будет проверить, имеет ли пользователь право работать с конечными точками API.

Испытания


Проверим то, что мы создали, предварительно запустив сервер приложения и сервер базы данных. А именно, если перейти по адресу http://localhost:3001/, то в окне терминала, где запущен сервер, можно будет увидеть сведения о запросе (там должно быть 200, что означает, что запрос прошёл успешно), и данные о времени ответа. Выглядеть это будет примерно так:



Приложение-клиент, то есть — браузер, должно вывести обычную страницу с текстом «Budget Manager API».

Проверим теперь маршрут route, доступ к которому можно получить по адресу http://localhost:3001/api/v1/auth.

В окне сервера появится сообщение о запросе GET со статусом 404 (это говорит о том, что с сервером связаться удалось, но он не может предоставить то, что нам нужно) и о времени ответа.



Происходит это из-за того, что мы используем данную конечную точку API только для POST-запросов. Серверу нечего ответить, если мы выполняем GET-запрос.

Проверим маршруты user, перейдя по адресу http://localhost:3001/api/v1/users. Сервер сообщит о методе GET со статусом 401. Это указывает на то, что запрос не был обработан, так как у нас недостаточно привилегий для работы с целевым ресурсом. Клиент выдаст страницу с текстом «Unauthorized».

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

Один из способов решить эту проблему заключается в использовании программы Postman. Её можно либо загрузить и установить как обычное приложение, либо пользоваться ей формате расширения для браузера Chrome.

Тестирование приложения с использованием Postman


Для начала подключимся к конечной точке setup для создания учётной записи администратора. В интерфейсе Postman это будет выглядеть так:



В поле для адреса введём http://localhost:3001/api/v1/setup, изменим тип запроса на POST и нажмём кнопку Send. В JSON-ответе сервера должно присутствовать сообщение "success": true.

Теперь попытаемся войти в систему с учётной записью администратора.



Для этого воспользуемся POST-запросом к конечной точке http://localhost:3001/api/v1/auth, на закладке Body зададим ключи username и password с одним и тем же значением admin и нажмём кнопку Send.

Ответ сервера должен выглядеть так, как показано на рисунке ниже.



Далее, получим список пользователей системы.



Для этого скопируем значение ключа token, воспользуемся GET-запросом, введя в поле адреса http://localhost:3001/api/v1/users, после чего добавим, на закладке Headers, новый заголовок Authorization со значением вида Bearer token (вместо token надо вставить токен, скопированный из ранее полученного ответа сервера). Там же добавим заголовок Content-Type со значением application/x-www-form-urlencoded и нажмём Send.

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



Проверим теперь метод регистрации нового пользователя, signup.



Для этого откроем новую вкладку, настроим POST-запрос к конечной точке http://localhost:3001/api/v1/signup, на закладке Body выберем переключатель x-www-form-urlencoded, введём ключи username и password со значениями, отличающимися от admin, и нажмём Send. Если всё работает как надо, в ответ должно прийти следующее:



Теперь, если мы вернёмся к вкладке Postman, на которой обращались к http://localhost:3001/api/v1/users для получения списка пользователей, и нажмём Send, в ответ должен прийти массив из двух объектов, представляющих администратора и нового пользователя.



Итоги


На этом мы завершаем первую часть данной серии. Здесь вы узнали о том, как, с чистого листа, создать Node.js-приложение и настроить простую JWT-аутентификацию. В следующей части приступим к разработке пользовательского интерфейса приложения с использованием Vue.js.

Уважаемые читатели! Как по-вашему, подойдёт ли способ аутентификации пользователей, предложенный автором, для использования в продакшне?