В начале этой серии руководств мы начали создавать наш сервер Node.js + Express, который будет использоваться для аутентификации пользователей и для обслуживания нашей клиентской игры Phaser. В первой части мы:
Вы можете скачать все файлы 2 части здесь.
Если вы не завершили 1 часть и хотели бы начать сразу с данной части, то вы можете найти код первой части здесь.
Давайте начнем!
Теперь, когда у нас есть Express-сервер, мы начнем добавлять новые конечные точки, которые потребуются на нашем сервере. Поскольку мы будем требовать от пользователей входа в систему, прежде чем они смогут получить доступ к нашей игре, нам нужно будет создать маршруты для следующих маршрутов:
Для наших новых маршрутов мы собираемся добавить эту логику к маршрутизатору, который мы создали в первой части этой серии руководств. В файл routes/main.js
добавьте следующий код ниже маршрута /status
:
router.post('/signup', (req, res, next) => {
res.status(200);
res.json({ 'status': 'ok' });
});
router.post('/login', (req, res, next) => {
res.status(200);
res.json({ 'status': 'ok' });
});
router.post('/logout', (req, res, next) => {
res.status(200);
res.json({ 'status': 'ok' });
});
router.post('/token', (req, res, next) => {
res.status(200);
res.json({ 'status': 'ok' });
});
В приведенном выше коде мы создали четыре новые точки, которые используют метод POST, и каждая из них возвращает ответ 200 в качестве плейсхолдера.
Далее нам нужно создать новый файл для маршрутов, которые мы хотим защитить. Выделение этих маршрутов в отдельный файл позволяет позже очень легко добавить новый маршрут. Для этого создайте новый файл в папке routes
с именем secure.js
и добавьте в этот файл следующий код:
const express = require('express');
const router = express.Router();
router.post('/submit-score', (req, res, next) => {
res.status(200);
res.json({ 'status': 'ok' });
});
router.get('/scores', (req, res, next) => {
res.status(200);
res.json({ 'status': 'ok' });
});
module.exports = router;
Наконец, нам нужно добавить эти новые маршруты в файл app.js
. Во-первых, нам нужно запросить файл, добавив следующую строку в начале файла рядом с другими операторами require
:
const secureRoutes = require('./routes/secure');
Во-вторых, нам нужно указать приложению использовать эти новые маршруты, и мы можем сделать это, добавив эту строку рядом с другим кодом маршрутов:
app.use('/', secureRoutes);
Теперь, если вы сохраните измененный код и перезапустите сервер, вы сможете достичь новых точек и получить ответ 200.
Учтите, что просто в браузере введя, например, http://localhost:3000/login, вы получите ошибку 404. Т.к. данная страница должна быть получена методом POST, а мы пытаемся получить ее методом GET. Таким способом вы можете проверить получение страницы http://localhost:3000/scores. Для запроса страницы методом POST далее мы будем использовать утилиту curl. С помощью нее вы можете проверить страницу например так: curl -X POST http://localhost:3000/login
.
К нашим новым маршрутам мы добавим логику подключения нашего сервера к нашему кластеру MongoDB Atlas, а для этого мы будем использовать Mongoose. Mongoose - это средство моделирование объектов базы данных MongoDB, предназначенное для асинхронной работы. Mongoose предоставляет решение на основе схемы для нашей модели данных, и включает в себя некоторые полезные функции, такие как проверка, построение запросов и многое другое из коробки. Прежде чем мы сможем начать использовать Mongoose, нам нужно будет установить этот модуль. Это мы можем сделать, запустив следующий код в терминале:
npm install --save mongoose
После установки пакета мы можем добавить соединение Mongo в файл app.js
. Для этого нам вначале нужно подключить модуль mongoose
, добавив следующую строку в начало файла рядом с другими операторами require
:
const mongoose = require('mongoose');
Затем добавьте следующий код сразу под операторами require
:
// установка mongo-соединения
const uri = process.env.MONGO_CONNECTION_URL;
mongoose.connect(uri, { useNewUrlParser : true, useCreateIndex: true, useUnifiedTopology: true });
mongoose.connection.on('error', (error) => {
console.log(error);
process.exit(1);
});
mongoose.connection.on('connected', function () {
console.log('connected to mongo');
});
В приведенном выше коде мы сделали следующее:
mongoose
.MONGO_CONNECTION_URL
(в которой мы храним нашу строку подключения к MongoDB Atlas) в новую переменную с именем uri
.connect
и передали ему два аргумента:
useNewUrlParser
- флаг, который указывает использовать новый URL, т.к. старый парсер уже сильно устарел. По умолчанию этот флаг установлен в false
.useCreateIndex
- флаг использования сборки индекса createIndex()
, а не устаревший драйвер MongoDB ensureIndex()
. По умолчанию этот флаг установлен в false
.useUnifiedTopology
- флаг использования нового механизма топологии. По умолчанию этот флаг установлен в false
.Если вы сохраните изменения в коде и перезапустите сервер, вы должны увидеть сообщение о подключении к mongo:
Теперь, когда наш сервер подключается к MongoDB, мы можем начать работу над логикой хранения и извлечения данных из базы данных. Для этого нам нужно определить схему и модель данных, которые мы будем хранить в MongoDB. Одним из преимуществ использования Mongoose является то, что он предоставляет для этого простое решение. Для каждого пользователя нам нужно будет сохранить следующие поля:
Чтобы создать схему и модель, создайте новую папку в корне вашего проекта с именем models
и в этой папке создайте новый файл с именем userModel.js
. Затем откройте userModel.js
и введите следующий код:
const mongoose = require('mongoose')
const bcrypt = require('bcrypt');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
email : {
type : String,
required : true,
unique : true
},
password : {
type : String,
required : true
},
name : {
type: String,
required: true
},
highScore : {
type: Number,
default: 0
}
});
UserSchema.pre('save', async function (next) {
const user = this;
const hash = await bcrypt.hash(this.password, 10);
this.password = hash;
next();
});
UserSchema.methods.isValidPassword = async function (password) {
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
const UserModel = mongoose.model('user', UserSchema);
module.exports = UserModel;
Давайте рассмотрим только что добавленный код:
mongoose
и bcrypt
. bcrypt - вспомогательная библиотека для хеширования паролей.mongoose.Schema
, который позволяет нам определять поля, которые должна иметь наша модель, и в каждом из этих полей мы можем указать их тип, если они требуются, и предоставить значения по умолчанию. Объект mongoose.Schema
предоставляет нам встроенное приведение типов и проверку. Например, если мы введем строку "12" вместо числа 12, mongoose автоматически преобразует эту строку в число.pre-save
ловушку (hook). Функции, которым передается управление во время выполнения асинхронных функций, называются hook
(ловушка, перхват, крюк). Наша функция для сохранения будет вызываться перед сохранением документа в MongoDB. Когда срабатывает эта ловушка, мы получаем ссылку на текущий документ, который будет сохранен, а затем мы используем bcrypt-хеширование пароля этого пользователя. Наконец, мы вызываем функцию обратного вызова, которая передается в качестве аргумента нашему хуку.isValidPassword
, который будет использоваться для проверки правильности ввода пароля пользователя при попытке входа в систему.mongoose.model
и передали этому методу два аргумента: имя нашей модели и схему, которая будет использоваться для модели. Затем мы экспортировали UserModel
.Прежде чем мы сможем начать использовать нашу новую модель, нам нужно установить пакет bcrypt
, и это можно сделать, выполнив в терминале следующую строку:
npm install bcrypt
Чтобы начать использовать нашу новую модель нам нужно обновить некоторые из маршрутов для создания, обновления и чтения данных из нашей базы данных. Первый маршрут, который мы собираемся обновить - это /signup
. В файле routes/main.js
замените код этого маршрута следующим:
router.post('/signup', asyncMiddleware( async (req, res, next) => {
const { name, email, password } = req.body;
await UserModel.create({ email, password, name });
res.status(200).json({ 'status': 'ok' });
}));
Затем в верхней части файла добавьте следующие строки под строкой require('express')
:
const asyncMiddleware = require('../middleware/asyncMiddleware');
const UserModel = require('../models/userModel');
В приведенном выше коде мы сделали следующее:
UserModel
и промежуточное ПО с именем asyncMiddleware
. Ниже мы напишем это промежуточное ПО.signup
мы поместили нашу функцию в asyncMiddleware
. Причина, по которой мы это сделали, заключается в том, что мы собираемся использовать async/await
в нашей функции, однако, чтобы гарантировать, что мы перехватываем любые неперехваченные ошибки в нашей функции, мы обычно заключаем нашу логику в оператор try/catch
. Однако, используя asyncMiddleware
, мы можем написать нашу логику без использования try/catch и позволить промежуточному программному обеспечению отлавливать любые неперехваченные ошибки.signup
, мы получаем поля name
, email
и password
из тела запроса, а затем мы отправляем эти аргументы в метод create
нашего объекта UserModel
. когда вызывается метод create
в нашей модели, mongoose
вызовет настроенный нами хук перед сохранением, после чего, mongoose
попытается добавить новый документ (запись) в базу данных.Прежде чем мы сможем протестировать наши изменения, нам нужно создать новое промежуточное ПО asyncMiddleware
. Для этого создайте новую папку в корне вашего проекта с именем middleware
и в этой папке создайте новый файл с именем asyncMiddleware.js
. В этот файл добавьте следующий код:
const asyncMiddleware = fn =>
(req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch(next);
};
module.exports = asyncMiddleware;
В приведенном выше коде мы создали новую функцию с именем asyncMiddleware
, которая принимает другую функцию в качестве аргумента и заключает ее в промис. В данном случае у нас эта передаваемая функция в промис является обработчиком маршрута. Если есть ошибка, она будет здесь перехвачена и передана следующему промежуточному программному обеспечению.
Теперь, если вы сохраните изменения кода и перезапустите сервер, вы можете протестировать обновленный маршрут signup
. Для этого вам нужно будет использовать curl
, Postman
или какой-либо другой метод для отправки запроса POST. В оставшейся части этого урока мы будем использовать curl
.
Если у вас операционная система Windows,в которой нет утилиты curl
, то вы можете ее скачать отсюда.
Сначала мы проверим правильность нашей модели mongoose. В терминале откройте новую вкладку или окно и введите следующий код:
curl -X POST \
http://localhost:3000/signup \
-H 'Content-Type: application/json' \
-d '{
"email": "test4@test.com",
"password": "1234",
"name2": "test4"
}'
После отправки данного запроса вы должны получить сообщение об ошибке о необходимости параметра name
:
Затем мы проверим, создает ли конечная точка данные в mongo
при отправке корректных данных в теле запроса. В терминале запустите следующий код:
curl -X POST \
http://localhost:3000/signup \
-H 'Content-Type: application/json' \
-d '{
"email": "test5@test.com",
"password": "1234",
"name": "test5"
}'
После отправки данного запроса вы должны получить сообщение: {"status":"ok"}
. Чтобы убедиться, что наш маршрут действительно сохранил данные в нашей базе данных, мы можем использовать пользовательский интерфейс MongoDB Atlas. Для этого войдите в MongoDB Atlas по адресу: https://cloud.mongodb.com/user#/atlas/login, а затем в разделе Clusters
нажмите копку Collections
:
Вы должны увидеть коллекцию users
с только что добавленным новым пользователем:
После создания маршрута для регистрации, записывающего данные в базу данных, мы можем обновить и некоторые другие маршруты. Первым маршрутом, на котором мы сосредоточимся, будет маршрут login
. В файле routes/main.js
замените всю логику маршрута login
следующим кодом:
router.post('/login', asyncMiddleware(async (req, res, next) => {
const { email, password } = req.body;
const user = await UserModel.findOne({ email });
if (!user) {
res.status(401).json({ 'message': 'unauthenticated' });
return;
}
const validate = await user.isValidPassword(password);
if (!validate) {
res.status(401).json({ 'message': 'unauthenticated' });
return;
}
res.status(200).json({ 'status': 'ok' });
}));
Давайте рассмотрим только что добавленный код:
asyncMiddleware
.email
и password
из тела запроса.findOne
модели UserModel
для запроса в базе данных первого пользователя, у которого есть адрес электронной почты, переданный в тело запроса.isValidPassword
для возвращенного объекта user
, чтобы проверить, совпадает ли пароль указанный в теле запроса с тем, что хранится в базе данных.Если вы сохраните изменения в коде и перезапустите сервер, вы сможете протестировать обновленную точку login
. Для этого запустите в своем терминале следующий код:
curl -X POST \
http://localhost:3000/login \
-H 'Content-Type: application/json' \
-d '{
"email": "test5@test.com",
"password": "1234"
}'
Вы должны получить ответ 200. Если вы измените поля password
или email
в теле и повторно отправите запрос, вы должны получить ответ 401.
Во второй части руководства мы сделали логику входа и авторизации. В 3 части мы продолжим обновлять существующие маршруты и сделаем следующее при: