Таблица лидеров. Часть 3.

Во второй части этой серии руководств мы продолжили работу над нашим сервером Express. Мы сделали следующее:

  • Добавили новые маршруты, которые понадобятся нашему серверу
  • Создали пользовательскую модель mongoose для нашей базы данных
  • Создали соединение для подключения к MongoDB
  • Начали обновление маршрутов для отправки и получения данных из MongoDB.

В части 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.
    • secretOrKey` используется для подписи создаваемого JWT. В этом руководстве мы использовали секретный заполнитель, но лучше извлечь его из переменных среды или использовать какой-либо другой безопасный метод.
    • 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.
  • Наконец, мы импортировали наш файл аутентификации, а затем обновили наши безопасные маршруты, чтобы использовать стратегию JWT 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 и функцию обратного вызова в качестве аргументов.
    • В функции обратного вызова мы создаем два веб-токена JSON с помощью библиотеки jsonwebtoken. Для JWT мы включаем идентификатор и адрес электронной почты пользователя в данные JWT, и мы устанавливаем срок действия основного токена в пять минут, а срок действия refreshToken - через один день.
    • Затем мы сохранили оба этих токена в объекте ответа, вызвав метод cookie, и сохранили эти токены в памяти, чтобы мы могли ссылаться на них позже при обновлении токена.

      Примечание: в этом руководстве мы храним эти токены в памяти, но на практике вы лучше хранить эти данные в постоянном хранилище определенного типа.

    • Наконец, мы ответили кодом 200, и в ответе отправили token и refreshToken.
  • Затем в маршруте token мы вытащили email и refreshToken из тела запроса. Затем мы проверили, есть ли refreshToken в массиве tokenList, который мы используем для отслеживания токенов пользователя, и убедились, что предоставленный email соответствует токену, хранящемуся в памяти.
    • Если они не совпадают или токен отсутствует в памяти, мы отвечаем кодом состояния 401.
    • Если они совпадают, мы создаем новый токен, сохраняем его в памяти и обновляем cookie ответа новым токеном.
    • Затем мы отвечаем кодом ответа 200 и в ответ отправляем новый token.
  • Наконец, в маршруте logout мы проверяем, есть ли у объекта req файлы cookie.
    • Если у объекта запроса есть какие-либо файлы cookie, мы извлекаем refreshJwt из файла cookie и удаляем его из нашего списка токенов в памяти, если он существует.
    • Затем удяляем куки jwt и refreshJwt с помощью вызова метода clearCookie объекта ответа.
    • Наконец, мы отвечаем кодом 200.

Теперь, когда маршруты обновлены, вы можете протестировать новую аутентификацию. Для этого, сохраните изменения кода и перезапустите сервер. Если вы попытаетесь отправив запрос по маршрутам submit-score и scores вы должны получить сообщение об ошибке 401 - Unauthorized (несанкционированный доступ). Если вы отправите запрос на маршрут login, вы должны получить ответ, содержащий символы token и refreshToken.

Вывод

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

Оригинал.