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