Во второй части этой серии руководств мы продолжили работу над нашим сервером Express. Мы сделали следующее:
В части 3 этой серии руководств мы продолжим работу на нашем сервере, обновив остальные конечные точки для отправки данных в MongoDB, и добавим аутентификацию для наших маршрутов.
Вы можете скачать все файлы, связанные с исходным кодом части 3 здесь.
Если вы не завершили 2 часть и хотите продолжить, вы можете найти код для нее здесь .
В настоящее время конечные точки /scores
и /submit-score
содежат только шаблонную логику, которая возвращает код со статусом 200. Теперь мы обновим маршрут /scores
, чтобы он получал десять лучших результатов из базы данных, и обновим маршрут /submit-score
, чтобы он обновлял результаты отдельного пользователя. Для этого откройте файл routes/secure.js
и замените весь код в файле следующим:
const express = require('express');
const asyncMiddleware = require('../middleware/asyncMiddleware');
const UserModel = require('../models/userModel');
const router = express.Router();
router.post('/submit-score', asyncMiddleware(async (req, res, next) => {
const { email, score } = req.body;
await UserModel.updateOne({ email }, { highScore: score });
res.status(200).json({ status: 'ok' });
}));
router.get('/scores', asyncMiddleware(async (req, res, next) => {
const users = await UserModel.find({}, 'name highScore -_id').sort({ highScore: -1}).limit(10);
res.status(200).json(users);
}));
module.exports = router;
Давайте рассмотрим только что добавленный код:
asyncMiddleware
и UserModel
, а затем обернули оба маршрута в функцию asyncMiddleware
, которую мы создали во второй части.submit-score
мы получили значения email
и score
из тела запроса. Затем мы использовали метод updateOne
модели UserModel
для обновления одной записи в базе данных, где предоставленное значение email
соответствует свойству email
в записи./scores
мы использовали метод find
модели UserModel
для поиска документов в базе данных. Метод find
принимает два аргумента:
_id
всегда возвращается, поэтому, чтобы исключить его, нам нужно использовать аргумент -_id
.sort
для сортировки возвращаемых результатов. Этот метод позволяет вам указать поле, по которому вы хотите выполнить сортировку, а, установив это значение в -1
результаты будут отсортированы в порядке убывания.limit
, чтобы ограничить вывод не более 10 записями.Теперь сохраните изменения и перезапустите сервер, вы сможете протестировать обновленные маршруты. Для этого нам понадобится утилита curl
, Postman
или какой-либо другой метод для отправки запроса POST. В оставшейся части этого урока мы будем использовать curl
.
Добавьте еще несколько пользователей в базу данных также как мы сделали это во 2 части. И для каждого пользователя в терминале введите код вроде этого:
curl -X POST \
http://localhost:3000/submit-score \
-H 'Content-Type: application/json' \
-d '{
"email": "test4@test.com",
"score": "100"
}'
После отправки запроса вы должны получить сообщение status ОК. Чтобы проверить маршрут /scores
, откройте новую вкладку в браузере и перейдите по следующему URL-адресу: http://localhost:3000/scores. Вы должны увидеть массив объектов , которые включают поля name
и highscore
:
Теперь мы начнем работать над добавлением аутентификации на наш сервер. Для аутентификации мы будем использовать библиотеку passport.js
вместе с модулем passport-jwt
. Passport
- это middleware для аутентификации для Nodejs, которое можно легко использовать вместе с Express. Кроме того, оно поддерживает множество различных видов аутентификации.
Наряду с passport.js
мы будем использовать JSON Web Token (JWT) для проверки пользователей. Помимо использования JWT для аутентификации пользователей, мы также будем использовать Refresh Token, чтобы позволить пользователю обновлять свой основной JWT. Причина, по которой мы делаем это, заключается в том, что мы хотим, чтобы основной JWT был недолговечным, и вместо того, чтобы требовать от игрока повторного входа в систему для получения нового токена, он может вместо этого использовать свой Refresh Token для обновления основного JWT, таким образом он будет долгоживущим.
Токены доступа (JWT) — это токены, с помощью которых можно получить доступ к защищенным ресурсам. Они короткоживущие, но многоразовые. В них может содержаться дополнительная информация, например, время жизни или IP-адрес, откуда идет запрос. Все зависит от желания разработчика. Refresh Token (RT) — эти токены выполняют только одну специфичную задачу — получение нового токена доступа. И на тут без сервера авторизации не обойтись. Они долгоживущие, но одноразовые.
Для начала на нужно установить необходимые пакеты. В терминале выполните следующую команду:
npm install --save cookie-parser passport passport-local passport-jwt jsonwebtoken
Вместе с библиотекой passport
мы устанавливаем две стратегии, которые нам понадобятся для нашего сервера. Стратегия passport-local
позволяет нам аутентифицироваться с помощью имени пользователя и пароля на нашем сервере, passport-jwt
позволяет нам аутентифицироваться с помощью веб-токена JSON. Наконец, middleware cookie-parser
позволяет нам анализировать заголовок cookie и заполнять его: в req.cookies
мы будем помещать сгенерированный JWT.
Для начала создайте новую папку в корневой папке проекта с именем auth
, а внутри нее создайте новый файл с именем auth.js
. В этот файл auth.js
добавьте следующий код:
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const JWTstrategy = require('passport-jwt').Strategy;
const UserModel = require('../models/userModel');
// обработка регистрации пользователя
passport.use('signup', new localStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
}, async (req, email, password, done) => {
try {
const { name } = req.body;
const user = await UserModel.create({ email, password, name});
return done(null, user);
} catch (error) {
done(error);
}
}));
В приведенном выше коде мы сделали следующее:
passport
, passport-local
и passport-jwt
. Затем мы импортировали файл userModel
.passport
на использование новой локальной стратегии при вызове маршрута signup
. Данная стратегия localStrategy
принимает два аргумента: объект options
и функцию обратного вызова.
options
мы устанавливаем поля usernameField
и passwordField
. По умолчанию, если эти поля не предусмотрены passport-local
будет ожидать поля username
и password
, и если мы хотим использовать другую комбинацию, мы должны предоставить ее.options
мы устанавливаем поле passReqToCallback
в true
, таким образом объект запроса req
будет передан функции обратного вызова.signup
и разместили ее здесь.done
, переданную в качестве аргумента функции обратного вызова.Прежде чем мы обновим сервер и маршруты для использования новой аутентификации, нам нужно будет завершить настройку passport. В auth.js
добавьте следующий код в конец файла:
// обработка входа пользователя
passport.use('login', new localStrategy({
usernameField: 'email',
passwordField: 'password'
}, async (email, password, done) => {
try {
const user = await UserModel.findOne({ email });
if (!user) {
return done(null, false, { message: 'User not found' });
}
const validate = await user.isValidPassword(password);
if (!validate) {
return done(null, false, { message: 'Wrong Password' });
}
return done(null, user, { message: 'Logged in Successfully' });
} catch (error) {
return done(error);
}
}));
// проверка валидности токена
passport.use(new JWTstrategy({
secretOrKey: 'top_secret',
jwtFromRequest: function (req) {
let token = null;
if (req && req.cookies) token = req.cookies['jwt'];
return token;
}
}, async (token, done) => {
try {
return done(null, token.user);
} catch (error) {
done(error);
}
}));
Давайте рассмотрим только что добавленный код:
login
. Затем в функции обратного вызова мы взяли логику входа пользователя в систему из маршрута login
и разместили ее здесь.passport
для использования новой стратегии JWT. Для JWTstrategy
мы передаем два аргумента: объект options
и функцию обратного вызова.
options
мы используем два поля: secretOrKey
и jwtFromRequest
.jwtFromRequest
- это функция, которая используется для получения jwt от объекта запроса. В этом руководстве мы будем помещать jwt в файл cookie
, поэтому в функции мы извлекаем jwt токен из файла cookie объекта запроса, если он существует, в противном случае мы возвращаемся null
.done
, которая была предоставлена для обратного вызова.Теперь, когда мы закончили настройку passport
, нам нужно обновить файл app.js
, чтобы он использовал эту библиотеку. Для этого откройте afqk app.js
и добавьте следующий код вверху файла рядом с другими операторами require
:
const cookieParser = require('cookie-parser');
const passport = require('passport');
Затем под строкой app.use(bodyParser.json());
добавьте следующий код :
app.use(cookieParser());
// подключаем passport-аутентификации
require('./auth/auth')
Наконец, замените эту строку: app.use('/', secureRoutes);
следующим кодом:
app.use('/', passport.authenticate('jwt', { session : false }), secureRoutes);
В приведенном выше коде мы сделали следующее:
cookie-parser
и passport
.cookie-parser
. При использовании cookie-parser
объект запроса будет включать файлы cookie.passport
, которую мы настроили.Последнее, что нам нужно сделать, прежде чем мы сможем протестировать изменения нашего кода - это обновить маршруты в routes/main.js
. Для этого откройте файл routes/main.js
и замените весь код в файле следующим кодом:
const passport = require('passport');
const express = require('express');
const jwt = require('jsonwebtoken');
const tokenList = {};
const router = express.Router();
router.get('/status', (req, res, next) => {
res.status(200).json({ status: 'ok' });
});
router.post('/signup', passport.authenticate('signup', { session: false }), async (req, res, next) => {
res.status(200).json({ message: 'signup successful' });
});
router.post('/login', async (req, res, next) => {
passport.authenticate('login', async (err, user, info) => {
try {
if (err || !user) {
const error = new Error('An Error occured');
return next(error);
}
req.login(user, { session: false }, async (error) => {
if (error) return next(error);
const body = {
_id: user._id,
email: user.email
};
const token = jwt.sign({ user: body }, 'top_secret', { expiresIn: 300 });
const refreshToken = jwt.sign({ user: body }, 'top_secret_refresh', { expiresIn: 86400 });
// сохраняем токены в cookie
res.cookie('jwt', token);
res.cookie('refreshJwt', refreshToken);
// сохраняем токены в памяти
tokenList[refreshToken] = {
token,
refreshToken,
email: user.email,
_id: user._id
};
//отправляем токены обратно пользователю
return res.status(200).json({ token, refreshToken });
});
} catch (error) {
return next(error);
}
})(req, res, next);
});
router.post('/token', (req, res) => {
const { email, refreshToken } = req.body;
if ((refreshToken in tokenList) && (tokenList[refreshToken].email === email)) {
const body = { email, _id: tokenList[refreshToken]._id };
const token = jwt.sign({ user: body }, 'top_secret', { expiresIn: 300 });
// обновляем jwt
res.cookie('jwt', token);
tokenList[refreshToken].token = token;
res.status(200).json({ token });
} else {
res.status(401).json({ message: 'Unauthorized' });
}
});
router.post('/logout', (req, res) => {
if (req.cookies) {
const refreshToken = req.cookies['refreshJwt'];
if (refreshToken in tokenList) delete tokenList[refreshToken]
res.clearCookie('refreshJwt');
res.clearCookie('jwt');
}
res.status(200).json({ message: 'logged out' });
});
module.exports = router;
Давайте рассмотрим только что добавленный код:
jsonwebtoken
и passport
, а затем удалили userModel
и asyncMiddleware
.signup
мы добавили промежуточное ПО passport.authenticate
и настроили его использование в signup
. Поскольку мы переместили всю логику для создания использования в файл auth.js
, единственное, что нам нужно сделать в функции обратного вызова - это вернуть ответ 200.login
мы добавили промежуточное ПО passport.authenticate
и настроили его использование в login
.
user
не был возвращен из промежуточного программного обеспечения passport
. Если эта проверка верна, мы создаем новую ошибку и передаем ее следующему промежуточному программному обеспечению.login
, который предоставляется в объекте req
. Этот метод добавляется паспортом автоматически. Когда мы вызываем этот метод, мы передаем объект user
, объект options
и функцию обратного вызова в качестве аргументов.jsonwebtoken
. Для JWT мы включаем идентификатор и адрес электронной почты пользователя в данные JWT, и мы устанавливаем срок действия основного токена в пять минут, а срок действия refreshToken - через один день.Примечание: в этом руководстве мы храним эти токены в памяти, но на практике вы лучше хранить эти данные в постоянном хранилище определенного типа.
token
мы вытащили email
и refreshToken
из тела запроса. Затем мы проверили, есть ли refreshToken
в массиве tokenList
, который мы используем для отслеживания токенов пользователя, и убедились, что предоставленный email соответствует токену, хранящемуся в памяти.
logout
мы проверяем, есть ли у объекта req
файлы cookie.
refreshJwt
из файла cookie и удаляем его из нашего списка токенов в памяти, если он существует.jwt
и refreshJwt
с помощью вызова метода clearCookie
объекта ответа.Теперь, когда маршруты обновлены, вы можете протестировать новую аутентификацию. Для этого, сохраните изменения кода и перезапустите сервер. Если вы попытаетесь отправив запрос по маршрутам submit-score
и scores
вы должны получить сообщение об ошибке 401 - Unauthorized (несанкционированный доступ).
Если вы отправите запрос на маршрут
login
, вы должны получить ответ, содержащий символы token
и refreshToken
.
Теперь, когда мы закончили настройку аутентификации пользователя, на этом заканчивается третья часть этого руководства. В 4 части мы начнем работу над нашей игрой, добавив страницу входа в систему, которая затем перенесет игрока в игру Phaser после входа в систему.