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