Освоение аутентификации на основе токенов в React и NodeJS

Эта статья изначально была опубликована по адресу https://whatisweb.dev/react-and-nodejs-authentication-with-refresh-access-tokens-a-step-by-step-guide.

Наиболее распространенный способ аутентификации в одностраничных приложениях (SPA) — использование веб-токенов JSON (JWT). Хотя аутентификацию на основе JWT легко реализовать, есть некоторые недостатки использования JWT в SPA, которые следует учитывать:

  1. Безопасность. Поскольку JWT не сохраняет состояние и не хранит никакой информации на сервере, невозможно сделать его недействительным со стороны сервера. Если JWT украден или скомпрометирован, злоумышленник может получить доступ к конфиденциальной информации.
  2. Отсутствие отзыва. Поскольку JWT не имеет состояния, это затрудняет отзыв токенов со стороны сервера, когда пользователь выходит из системы или меняет свой пароль, или позволяет пользователям выходить из системы со всех устройств.
  3. Ограниченный период действия. Поскольку JWT действует в течение ограниченного периода времени, пользователям придется входить в приложение каждый раз, когда истечет срок действия JWT, что не очень удобно для пользователей.

Мы можем избежать этих проблем, либо 1) используя сеансы, либо 2) используя токены обновления и доступа для аутентификации пользователей, о чем я расскажу в этой статье.

Прежде чем начать, давайте сначала разберемся, что такое токены обновления и доступа.

Что такое токены доступа?

Токены доступа — это недолговечные токены, которые выпускаются сервером авторизации и используются для доступа к защищенным ресурсам. Маркеры доступа передаются в заголовке запроса API и используются для проверки наличия у пользователя необходимых разрешений для доступа к запрошенному ресурсу.

Что такое токены обновления?

Токены обновления — это долгоживущие токены, которые выпускаются вместе с токенами доступа. Они используются для запроса нового токена доступа по истечении срока действия текущего токена доступа. Это позволяет пользователю оставаться аутентифицированным, даже если срок действия токена доступа истек, без необходимости повторного ввода своих учетных данных.

Начиная

Перед запуском на вашем компьютере должен быть установлен npm, который поставляется в комплекте с Node.js, который вы можете установить здесь.

Структура папок:

Вы можете создать вышеуказанные каталоги с помощью этих команд.

mkdir refresh-token-auth-app
cd refresh-token-auth-app
mkdir client server

Этот проект разделен на две части:

  1. Приложение Node.js и Express для серверной части.
  2. Приложение ReactJS для внешнего интерфейса.

Создание внутреннего приложения:

Выполните следующие команды, чтобы инициализировать проект Node.js и установить необходимые зависимости.

cd refresh-token-auth-app/server
npm init -y
npm install express jsonwebtoken cookie-parser cors dotenv ms http-errors

Создание внешнего интерфейса:

Выполните следующие команды, чтобы инициализировать проект React.

cd refresh-token-auth-app
npx create-react-app client

Поток аутентификации

Теперь, когда мы настроили структуру проекта, необходимую для приложения, давайте разберемся с общим обзором того, как обновление и проверка подлинности на основе токена доступа будут работать в нашем приложении.

  1. Когда пользователь впервые входит в наше приложение, сервер генерирует для пользователя токен доступа и токен обновления. Токен доступа будет отправлен как часть тела ответа, а токен обновления будет отправлен как httpOnly файл cookie. Токен обновления также будет храниться в базе данных для каждого пользователя.
  2. Когда срок действия токена доступа подходит к концу, наше приложение автоматически отправит запрос на сервер для обновления токена доступа, что также называется тихой аутентификацией. Поскольку мы храним токен доступа в памяти, а не в локальном хранилище, чтобы предотвратить XSS-атаки, наше приложение также будет выполнять автоматическую аутентификацию, когда пользователь обновляет окно браузера.
  3. Когда пользователь выходит из приложения, сервер удаляет токен обновления, хранящийся в базе данных, и файл cookie.

Конечные точки API

Теперь, когда у нас есть общее представление о потоке аутентификации нашего приложения, давайте перечислим конечные точки API, необходимые для работы приложения.

Нам понадобятся эти конечные точки, чтобы наше приложение работало.

  1. POST - /auth/sign-up, чтобы разрешить пользователям регистрироваться.
  2. POST - /auth/login, чтобы пользователи могли войти в систему.
  3. POST - /auth/refresh для создания нового токена доступа с использованием данного токена обновления.
  4. POST - /auth/logout, чтобы выйти из системы.
  5. GET - /users/list (необязательно), чтобы получить список пользователей.

Структура папок

Прежде чем мы начнем писать эти API, давайте создадим структуру папок, необходимую для серверного приложения.

Вы можете создать вышеуказанные каталоги с помощью этих команд.

cd server
mkdir controllers middlewares routes data utils
touch app.js

Создание экспресс-приложения

Добавьте следующий шаблонный код в файл app.js, чтобы создать экспресс-приложение.

const path = require('path');
require('dotenv').config();

const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');

const { PORT, NODE_ENV } = process.env;

const isDev = NODE_ENV === 'development';

const app = express();

if (isDev) {
    app.use(
        cors({
            origin: 'http://localhost:3000',
            optionsSuccessStatus: 200,
            credentials: true,
        })
    );
}

app.use(express.json({ type: 'application/json' }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(express.static(path.join(__dirname, 'public')));

app.get('*', (req, res) => {
    res.sendFile(path.resolve(__dirname, 'public', 'index.html'));
});

app.use((req, res, next) => {
    const error = new Error('Not Found');
    error.status = 404;
    next(error);
});

app.use((error, req, res, next) => {
   console.error("\x1b[31m", error);
   if (res.headersSent) {
        return next(error);
    }
    return res.status(error.status || 500).json({
        error: {
            status: error.status || 500,
            message: error.status ? error.message : "Internal Server Error",
        },
    });
});

app.listen(PORT || 5000, (error) => {
    if (error) {
        console.log("Error in server setup");
        return;
    }
    console.log("Server listening on Port", PORT);
});

Здесь я использую:

  1. промежуточное ПО dotenv для загрузки переменных среды из файла .env в переменную process.env.
  2. cors промежуточное программное обеспечение для обеспечения общего доступа к ресурсам между источниками (CORS) в режиме разработки.
  3. express.json для анализа тела запроса в формате JSON.
  4. cookie-parser промежуточное программное обеспечение для анализа файлов cookie в заголовках входящих запросов.
  5. промежуточное ПО express.static для обслуживания статических файлов из каталога public.
  6. Промежуточное программное обеспечение обработчика ошибок, которое выполняется, когда в приложении возникает ошибка.
  7. И, наконец, метод app.listen для создания нового сервера и прослушивания указанного порта, определенного переменной среды PORT.

Добавьте этот код в раздел сценария package.json:

"start": "node app.js"

Переменные среды

Создайте файл .env в корне каталога server и добавьте в этот файл следующие переменные среды.

PORT=5000

ACCESS_TOKEN_SECRET=7rG7v5ElkhMpIHdQfs5l4sC+zprSYD2DNII4fzRusLevT2n0fEvpFzd6Ei2GpXzwEkghDxWxONRx0eCvcrsziY6EuF6GutZX+niTT6QJylTba/ydgURY9+7k1rn8w7sfiCAQPBg7c/SlY/nMRsDF4/5MSQATlfuSXX+9BIKgDmFWwZA19QqGS4cWKNiQO7JEhcNjkpy0FtaeUzK1/q0pG5Rjq8V8L8zbyhttUbAWd3h8N+m5vV7gi22HBrLlqpbFL0IIeb3GHWEe9z1nymyQNjLdxO6kcNRBNmWR7nRbamje6TJ6aHChebONL5h3GRWAFLwS188L41iNp67EqcNSqg==

REFRESH_TOKEN_SECRET=LbIiOVV6MuKQ7A2KnGi6uW6vxrypJrouog48VY4bJjrJJdBbq0XLuKBU4Ia/Pzphvk4j6iUa7EEFnpgCBRewvxPCIyHZpHrGRZjUtCmbjGpLqIe5tlgMlEOPTzrwYAkgAHBNN6UzeZl55wlzOSiCWbhqcw2V6qDy8KYh+llIm/eBUVVlThNw7TDsn0LtcLBjhkzaBQqCUzZQmOLtTpCerjnzaWzlS2vSyP96zJ/yemlkgF21EZkiKPdoNrJPeaXNk4kqECHNlZ4mccpCTSWPr+RPR/vjGltCRL6nhJ1w6MqBDYFpXcAHcv54fz1bXcEwkhEO5imzoKa6aMg/1LPpTw==

COOKIE_SECRET=Ak1jjwP38UQ3TUPatxFva2tytaYx0HnKxkfytoQAoignerppxxg7ogh2tUxnKhSe0JXVL7kbAZsHcnCFE3hY3OI2nuydrR9JL/xrj30EFBSQNjQ7FK8rY8S0QES/5z28k+etbBd9u7ms/bo/+YuA2ueJ2MiFCeRNH7UGknueF3JHEa+sfSVsf3QLIBgXd2WwmemRNYeqtpRmdxY29t8HeDwJqtoY9WdLU2onahQCzyuzD2/5aJWwwSIGyL7VeHSg7BQ/DDK+s2tv/IP6LVr3kVGMwhOGJksh6N5Ndeh9p22BkxsN4Nw1jzlxRGN4OhNmLdiPkFsAzj2B739z87mwNQ==

ACCESS_TOKEN_LIFE=15m

REFRESH_TOKEN_LIFE=30d

Вы можете сгенерировать эти секретные ключи в консоли NodeJS/REPL (Read Evaluate Print Loop), используя:

require('crypto').randomBytes(256).toString('base64');

Поскольку токен доступа предназначен для краткосрочного использования, срок его действия истекает через 15 минут. Токен обновления должен оставаться действительным в течение более длительного времени, поэтому срок его действия установлен на 30 дней.

Добавление фиктивных данных

Для простоты я не использую в этой статье базу данных, поэтому давайте создадим новый файл data.js внутри папки данных. Этот файл содержит фиктивные данные, необходимые для работы приложения.

// data.js
const users = [
    {
        id: 1,
        name: "John Doe",
        email: "[email protected]",
        userName: "johndoe",
        password: "JohnDoe@123"
    },
    {
        id: 2,
        name: "Jane Smith",
        email: "[email protected]",
        userName: "janesmith",
        password: "JaneSmith@123"
    },
];

const tokens = [];  // [{userId: number, refreshToken: string, expirationTime: number }]

module.exports = { users, tokens };

Здесь массив users используется для хранения пользователей приложения, а массив tokens используется для хранения токенов обновления пользователей вместе с их идентификатором пользователя и временем истечения срока действия токена.

Примечание. Я храню пароли в виде обычного текста, но вам не следует хранить их в виде обычного текста для реальных приложений.

Добавление маршрутов, контроллеров и промежуточных программ

Добавление маршрутов аутентификации

Добавьте этот код в файл auth.js внутри папки routes.

const router = require('express').Router();

const authController = require('../controllers/auth');
const authMiddleware = require('../middlewares/auth');

router.post(
    '/sign-up',
    authController.signUp,
    authMiddleware.generateAuthTokens
);

router.post(
    '/login',
    authController.login,
    authMiddleware.generateAuthTokens
);

router.post(
    '/logout',
    authMiddleware.isAuthenticated,
    authController.logout
);

router.post(
    '/refresh',
    authController.refreshAccessToken
);

module.exports = router;
  1. POST /auth/sign-up: Эта конечная точка обрабатывает запросы на регистрацию. Он использует метод authController.signUp для создания новой учетной записи пользователя, а затем использует промежуточное ПО authMiddleware.generateAuthTokens для создания токенов аутентификации для пользователя.
  2. POST /auth/login: Эта конечная точка обрабатывает запросы на вход. Он использует метод authController.login для аутентификации пользователя, а затем использует промежуточное ПО authMiddleware.generateAuthTokens для создания токенов аутентификации для пользователя.
  3. POST /auth/logout: Эта конечная точка обрабатывает запросы пользователей на выход из системы. Он использует промежуточное ПО authMiddleware.isAuthenticated для проверки подлинности пользователя, а затем использует метод authController.logout для выхода пользователя из системы.
  4. POST /auth/refresh: эта конечная точка используется для обновления токена доступа. По истечении срока действия маркера доступа клиент может использовать эту конечную точку для получения нового маркера доступа, отправив действительный маркер обновления в тексте запроса. Функция authController.refreshAccessToken вызывается для обработки этого запроса и создания нового токена доступа.

Добавление маршрутов пользователей

Добавьте этот код в файл users.js внутри папки routes.

const router = require('express').Router();

const { isAuthenticated } = require('../middlewares/auth');

const usersController = require('../controllers/users');

router.get('/list', isAuthenticated, usersController.getUsersList);

router.get('/me', isAuthenticated, usersController.getAuthenticatedUser);

router.get('/:id', isAuthenticated, usersController.getUserById);

module.exports = router;
  1. GET /users/list: Эта конечная точка используется для получения списка пользователей. Он использует промежуточное ПО authMiddleware.isAuthenticated для проверки подлинности пользователя, а затем использует метод usersController.getUsersList для получения списка пользователей.
  2. GET /users/me: Эта конечная точка используется для получения информации о аутентифицированном пользователе. Он использует промежуточное ПО authMiddleware.isAuthenticated для проверки подлинности пользователя, а затем использует метод usersController.getAuthenticatedUser для получения аутентифицированного пользователя.
  3. GET /users/:id: Эта конечная точка используется для получения информации о пользователе по идентификатору. Он использует промежуточное ПО authMiddleware.isAuthenticated для проверки подлинности пользователя, а затем использует метод usersController.getUserById для получения пользователя по его идентификатору. Идентификатор пользователя указывается в URL-пути с использованием параметра маршрута (:id).

Обновление app.js:

Добавьте этот код внутрь app.js, чтобы добавить маршруты auth и users.

/**
 * Existing Code
 */
const path = require('path');
require('dotenv').config();

const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');

/**
 * New Code
 */
const authRoutes = require('./routes/auth');
const usersRoutes = require('./routes/users');

/**
 * Existing Code
 */
const { PORT, NODE_ENV } = process.env;

const isDev = NODE_ENV === 'development';

const app = express();

if (isDev) {
    app.use(
        cors({
            origin: 'http://localhost:3000',
            optionsSuccessStatus: 200,
            credentials: true,
        })
    );
}

app.use(express.json({ type: 'application/json' }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(express.static(path.join(__dirname, 'public')));

/**
 * New Code
 */
app.use('/api/auth', authRoutes);
app.use('/api/users', usersRoutes);

Добавление контроллеров аутентификации

Добавьте этот код в файл auth.js внутри папки controllers.

Контроллер регистрации

const createError = require('http-errors');

const { users } = require('../data/data');

const signUp = async (req, res, next) => {
    const { name, username, email, password } = req.body;

    if(!name || !username || !email || !password) {
        return res.status(422).json({
            error: 'Please fill all the required fields'
        });
    }

    try {
        const userAlreadyExists = users.find(user => {
            if (user.userName === username || user.email === email) {
                return true;
            }
            return false;
        });

        if (userAlreadyExists) {
            return res.status(422).json({
                error: 'Username or email already exists'
            });
        }

        const newUser = {
            id: users[users.length - 1].id + 1,
            name: name,
            userName: username,
            email: email,
            password: password
        };

        users.push(newUser);

        req.userId = newUser.id;
        return next();
    } catch (error) {
        return next(error);
    }
};

module.exports = {
    signUp
}

Функция signUp отвечает за обработку процесса регистрации. Для этого требуется, чтобы имя пользователя, имя пользователя, адрес электронной почты и пароль были отправлены в теле запроса. Затем он проверяет наличие всех обязательных полей и, если нет, возвращает ответ с кодом состояния 422 и сообщением об ошибке.

Затем он проверяет, существует ли уже пользователь с таким же username или email в массиве users. Если это так, он возвращает ответ с кодом состояния 422 и сообщением об ошибке, указывающим, что имя пользователя или адрес электронной почты уже существует.

Если пользователь еще не существует, функция создает новый объект пользователя с необходимыми полями, присваивает ему уникальный id и помещает в массив users.

Затем функция устанавливает свойство userId объекта req в значение id вновь созданного пользователя и вызывает функцию next для передачи управления следующему промежуточному программному обеспечению для создания маркеров аутентификации для пользователя.

Если во время выполнения функции возникает какая-либо ошибка, она вызывает функцию next с ошибкой, чтобы передать управление промежуточному программному обеспечению обработки ошибок.

Контроллер входа в систему

const createError = require('http-errors');

const { users } = require('../data/data');

const signUp = async (req, res, next) => { .... };

const login = async (req, res, next) => {
    const { username, password } = req.body;

    try {
        if (!username || !password) {
            return res.status(422).json({
                error: 'Please fill all the required fields'
            });
        }

        const user = users.find(user => {
            if (user.userName === username || user.email === username) {
                return true;
            }
            return false;
        });

        if (!user) {
            const error = createError.Unauthorized('Invalid username or password');
            throw error;
        }

        const passwordsMatch = user.password == password;

        if (!passwordsMatch) {
            const error = createError.Unauthorized('Invalid username or password');
            throw error;
        }

        req.userId = user.id;
        return next();
    } catch (error) {
        return next(error);
    }
};

module.exports = {
    signUp,
    login
}

Функция login отвечает за обработку процесса входа в систему. Он требует, чтобы имя пользователя и пароль были отправлены в теле запроса. Если какое-либо из этих полей отсутствует, клиент отправляет ответ об ошибке, указывающий, что оба поля являются обязательными.

Затем функция ищет пользователя в массиве users, проверяя, соответствует ли userName или email пользователя предоставленному username. Если соответствующий пользователь найден, функция проверяет, соответствует ли предоставленный password паролю пользователя.

Если предоставленный password не совпадает, функция отправляет ответ об ошибке, указывающий, что имя пользователя или пароль недействительны. Если предоставленное password соответствует, функция затем устанавливает свойство userId объекта req в id аутентифицированного пользователя и вызывает функцию next для передачи управления следующему промежуточному программному обеспечению для создания маркеров аутентификации для пользователя.

Если во время выполнения функции возникает какая-либо ошибка, она вызывает функцию next с ошибкой, чтобы передать управление промежуточному программному обеспечению обработки ошибок.

Контроллер выхода

const jwt = require('jsonwebtoken');
const createError = require("http-errors");
const ms = require('ms');

const { clearTokens, generateJWT } = require("../utils/auth");
const { users, tokens } = require("../data/data");

const signUp = async (req, res, next) => { .... };

const login = async (req, res, next) => { .... };

const logout = async (req, res, next) => {
  await clearTokens(req, res, next);
  return res.sendStatus(204);
};

module.exports = {
    signUp,
    login,
    logout
};

Функция logout отвечает за выход пользователя из системы. Это достигается путем вызова функции clearTokens для удаления маркера обновления пользователя с сервера, а также из файла cookie пользователя.

Как только функция clearTokens завершает свое выполнение, функция logout отправляет ответ с кодом состояния HTTP 204 (что означает «Нет содержимого») обратно клиенту.

Обновить контроллер токена доступа

const jwt = require('jsonwebtoken');
const createError = require("http-errors");
const ms = require('ms');

const { clearTokens, generateJWT } = require("../utils/auth");
const { users, tokens } = require("../data/data");

const signUp = async (req, res, next) => { .... };

const login = async (req, res, next) => { .... };

const logout = async (req, res, next) => { .... };

const refreshAccessToken = async (req, res, next) => {
  const { REFRESH_TOKEN_SECRET, ACCESS_TOKEN_SECRET, ACCESS_TOKEN_LIFE } = process.env;

  const { signedCookies } = req;
  const { refreshToken } = signedCookies;
  if (!refreshToken) {
    return res.sendStatus(204);
  }
  try {
    const refreshTokenInDB = tokens.find(token => token.refreshToken == refreshToken)?.refreshToken;

    if (!refreshTokenInDB) {
      await clearTokens(req, res, next);
      const error = createError.Unauthorized();
      throw error;
    }

    try {
      const decodedToken = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
      const { userId } = decodedToken;
      const user = users.find(user => user.id == userId);

      if (!user) {
        await clearTokens(req, res);
        const error = createError("Invalid credentials", 401);
        throw error;
      }

      const accessToken = generateJWT(
        user.id,
        ACCESS_TOKEN_SECRET,
        ACCESS_TOKEN_LIFE
      );
      return res.status(200).json({
        user,
        accessToken,
        expiresAt: new Date(Date.now() + ms(ACCESS_TOKEN_LIFE)),
      });
    } catch (error) {
      return next(error);
    }
  } catch (error) {
    return next(error);
  }
};

module.exports = {
    signUp,
    login,
    logout,
    refreshAccessToken
};

Эта функция refreshAccessToken отвечает за обновление токена доступа пользователя. Функция сначала проверяет маркер обновления в подписанных файлах cookie запроса. Если найден токен обновления, он проверяет, действителен ли токен, сравнивая его с токенами, хранящимися в массиве tokens.

Если токен недействителен, он очищает все токены и выдает ошибку с кодом состояния 401. Если токен действителен, он декодирует токен обновления, чтобы получить идентификатор пользователя, проверяет, действителен ли пользователь, создает новый токен доступа и возвращает его вместе с информацией о пользователе и сроком действия. Если в процессе возникает ошибка, функция передает ошибку следующей функции промежуточного программного обеспечения для обработки ошибок.

Добавление контроллеров пользователей

Добавьте этот код в файл users.js внутри папки controllers.

const createError = require('http-errors');

const { users } = require('../data/data');

const getUsersList = async (req, res, next) => {
    const usersListWithOutPassword = users.map(user => {
        const {password, ...userWithOutPassword} = user;
        return {...userWithOutPassword};
    });

    return res.status(200).json({
        data: usersListWithOutPassword
    })
};

const getAuthenticatedUser = async (req, res, next) => {
    try {
        const { userId } = req;
    
        const authenticatedUser = users.find(user => user.id == userId);
    
        if(authenticatedUser) {
            return res.status(200).json({
                data: authenticatedUser
            })
        }
    
        const error = createError.NotFound();
        throw error;

    } catch(error) {
        return next(error);
    }

};

const getUserById = async (req, res, next) => {
    try {
        const { id } = req.params;
    
        const user = users.find(user => user.id == id);
    
        if (user) {
            return res.status(200).json({
                data: user
            })
        }
    
        const error = createError.NotFound();
        throw error;
    } catch(error) {
        return next(error);
    }
};

module.exports = {
    getUsersList,
    getAuthenticatedUser,
    getUserById
}
  1. getUsersList: Эта функция возвращает список всех пользователей.
  2. getAuthenticatedUser: Эта функция возвращает текущего аутентифицированного пользователя на основе его userId.
  3. getUserById: Эта функция возвращает пользователя по его идентификатору.

Добавление ПО промежуточного слоя аутентификации

Добавьте этот код в файл auth.js внутри папки middlewares.

generateAuthTokens промежуточное ПО

Это промежуточное ПО используется для создания токенов доступа и обновления после того, как пользователь успешно зарегистрируется или войдет в наше приложение.

const jwt = require('jsonwebtoken');
const createError = require('http-errors');
const ms = require('ms');

const { generateJWT } = require('../utils/auth');

const { ACCESS_TOKEN_LIFE, REFRESH_TOKEN_LIFE, ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, NODE_ENV } = process.env;

const dev = NODE_ENV === 'development';

const { users, tokens } = require('../data/data');

const generateAuthTokens = async (req, res, next) => {
    try {
        const user = users.find(user => user.id === req.userId);

        const refreshToken = generateJWT(
            req.userId,
            REFRESH_TOKEN_SECRET,
            REFRESH_TOKEN_LIFE
        );

        const accessToken = generateJWT(
            req.userId, 
            ACCESS_TOKEN_SECRET, 
            ACCESS_TOKEN_LIFE
        );

        const token = {
            refreshToken,
            userId: req.userId,
            expirationTime: new Date(Date.now() + ms(REFRESH_TOKEN_LIFE)).getTime(),
        };

        tokens.push(token);

        res.cookie("refreshToken", refreshToken, {
            httpOnly: true,
            secure: !dev,
            signed: true,
            expires: new Date(Date.now() + ms(REFRESH_TOKEN_LIFE)),
        });

        const expiresAt = new Date(Date.now() + ms(ACCESS_TOKEN_LIFE));

        return res.status(200).json({
            user,
            token: accessToken,
            expiresAt,
        });
    } catch (error) {
        return next(error);
    }
};

module.exports = {
    generateAuthTokens
}

Токены обновления и доступа генерируются путем вызова функции generateJWT с идентификатором аутентифицированного пользователя, секретом токена и временем истечения срока действия токена.

Здесь я отправляю токен доступа как часть тела ответа вместе со временем его истечения и объектом аутентифицированного пользователя, а также отправляю токен обновления в виде файла cookie httpOnly. Я также сохраняю токен обновления в базе данных (здесь массив tokens), чтобы аннулировать сеанс пользователя из бэкэнда.

Примечание. Я отправляю токен обновления в виде файла cookie httpOnly, чтобы предотвратить его чтение с помощью JavaScript, и устанавливаю для атрибута secure значение false в режиме разработки и значение true в рабочей среде, чтобы отправлять файлы cookie только с использованием HTTPS в производство.

isAuthenticated Промежуточное ПО

Это промежуточное ПО используется для проверки подлинности пользователя для доступа к защищенным маршрутам.

const jwt = require('jsonwebtoken');
const createError = require('http-errors');
const ms = require('ms');

const { generateJWT } = require('../utils/auth');

const { ACCESS_TOKEN_LIFE, REFRESH_TOKEN_LIFE, ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, NODE_ENV } = process.env;

const dev = NODE_ENV === 'development';

const { users, tokens } = require('../data/data');

const generateAuthTokens = async (req, res, next) => { ... };

const isAuthenticated = async (req, res, next) => {
    try {
        const authToken = req.get('Authorization');
        const accessToken = authToken?.split('Bearer ')[1];
        if (!accessToken) {
            const error = createError.Unauthorized();
            throw error;
        }

        const { signedCookies = {} } = req;

        const { refreshToken } = signedCookies;
        if (!refreshToken) {
            const error = createError.Unauthorized();
            throw error;
        }

        let refreshTokenInDB = tokens.find(token => token.refreshToken === refreshToken);
        
        if (!refreshTokenInDB) {
            const error = createError.Unauthorized();
            throw error;
        }

        refreshTokenInDB = refreshTokenInDB.refreshToken;

        let decodedToken;
        try {
            decodedToken = jwt.verify(accessToken, ACCESS_TOKEN_SECRET);
        } catch (err) {
            const error = createError.Unauthorized();
            return next(error);
        }

        const { userId } = decodedToken;

        const user = users.find(user => user.id == userId);
        if (!user) {
            const error = createError.Unauthorized();
            throw error;
        }

        req.userId = user.id;
        return next();
    } catch (error) {
        return next(error);
    }
};

module.exports = {
    generateAuthTokens,
    isAuthenticated
}

Эта промежуточная функция проверяет, аутентифицирован ли входящий запрос. Он делает это, сначала проверяя, имеет ли запрос токен доступа в заголовке авторизации. Если это не так, функция выдает ошибку, указывающую, что запрос неавторизован.

Если в запросе есть токен доступа, функция промежуточного программного обеспечения проверяет наличие токена обновления в подписанном файле cookie. Если маркер обновления отсутствует, функция выдает ошибку, указывающую на то, что запрос неавторизован.

Если в запросе есть и токен доступа, и токен обновления, функция промежуточного ПО проверяет, присутствует ли токен обновления в базе данных. Если это не так, функция выдает ошибку, указывающую, что запрос неавторизован.

Если маркер обновления присутствует в базе данных, функция промежуточного программного обеспечения пытается декодировать маркер доступа, используя секрет маркера доступа. Если декодирование не удается, функция выдает ошибку, указывающую на то, что запрос неавторизован.

Если декодирование прошло успешно, функция промежуточного программного обеспечения извлекает идентификатор пользователя из декодированного токена и проверяет, есть ли в базе данных пользователь с таким идентификатором. Если пользователя нет, функция выдает ошибку, указывающую на то, что запрос не авторизован.

Если пользователь найден, функция промежуточного программного обеспечения устанавливает для свойства userId объекта запроса идентификатор пользователя и вызывает функцию next() для передачи управления следующей функции промежуточного программного обеспечения в цепочке.

Добавление служебных функций

Добавьте этот код в файл auth.js внутри каталога utils.

const jwt = require('jsonwebtoken');

const { tokens } = require('../data/data');

const dev = process.env.NODE_ENV === 'development';

const generateJWT = (userId, secret, expirationTime) => {
    return jwt.sign(
        {
            userId,
        },
        secret,
        { expiresIn: expirationTime }
    );
}
const clearTokens = async (req, res) => {
    const { signedCookies = {} } = req;
    const { refreshToken } = signedCookies;
    if (refreshToken) {
        const index = tokens.findIndex(token => token.refreshToken === refreshToken);
        if(index) {
            tokens.splice(index, 1);
        }
    }
    res.clearCookie('refreshToken', {
        httpOnly: true,
        secure: !dev,
        signed: true,
    });
};

module.exports = {
    generateJWT,
    clearTokens
};
  1. generateJWT: эта функция используется для создания токена доступа и обновления с помощью библиотеки jsonwebtoken.
  2. clearTokens: эта функция используется для очистки маркера обновления из базы данных и файла cookie, когда пользователь выходит из приложения.

Создание React-приложения

Выполните следующие команды, чтобы инициализировать приложение React и установить необходимые зависимости.

cd refresh-token-auth-app
npx create-react-app client
cd client
npm install axios react-router-dom react-hook-form
npm install --save-dev sass

Структура папок

Прежде чем мы начнем писать какой-либо код, давайте создадим структуру папок, необходимую для внешнего приложения.

Вы можете создать вышеуказанные каталоги с помощью этих команд.

cd client/src
mkdir components contexts utils

(Необязательно) Добавление шрифтов и стилей

  • Добавьте этот шрифт в раздел head файла index.html в каталоге public.
<link href="https://fonts.googleapis.com/css2?family=Urbanist:wght@300;400;500;700&display=swap" rel="stylesheet" />
  • Замените код CSS внутри index.css кодом ниже:
*::before,
*,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: inherit;
}

body {
  margin: 0;
  font-family: 'Urbanist', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
  • И, наконец, удалите код внутри файла App.css или удалите App.css, так как этот файл нам не понадобится в этом уроке.

Прокси-запросы API

Добавьте эту строку кода в файл package.json, если вы хотите писать запросы типа axios.post('/api/auth/sign-up') вместо axios.post('http://localhost:5000/api/auth/sign-up')

  "proxy": "http://localhost:5000"

Компонент регистрации

Давайте начнем с создания компонента регистрации, который позволяет пользователям регистрироваться в нашем приложении, заполнив регистрационную форму.

Примечание. Мы будем использовать модули CSS для оформления нашего приложения React. Поэтому для каждого компонента мы также создадим файл CSS или SCSS для этого компонента.

Signup.js

Создайте новую папку Signup в каталоге components и создайте новый файл Signup.js в этой новой папке.

import { Link } from "react-router-dom";
import { useForm } from "react-hook-form";

import styles from "./Signup.module.scss";

const Signup = () => {
  const {
    handleSubmit,
    register,
    formState: { errors, touchedFields },
  } = useForm({
    defaultValues: {
      name: "",
      username: "",
      email: "",
      password: "",
      confirmPassword: "",
    },
    mode: "onChange",
  });

  const onSubmit = async (values) => {};

  return (
    <div className={styles.container}>
      <div className={styles.formWrapper}>
        <form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
          <h1 className={styles.formTitle}>Create New Account</h1>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="text"
              name="name"
              id="name"
              placeholder="Name"
              {...register("name", {
                required: { value: true, message: "Name is required." },
                minLength: { value: 2, message: "Name cannot be less than 2 characters" },
                maxLength: { value: 30, message: "Name cannot be more than 30 characters" },
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.name && errors.name?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="text"
              name="username"
              id="username"
              placeholder="Username"
              {...register("username", {
                required: { value: true, message: "Username is required." },
                minLength: { value: 2, message: "Username cannot be less than 2 characters" }
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.username && errors.username?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="email"
              name="email"
              id="email"
              autoComplete="email"
              placeholder="Email"
              {...register("email", {
                required: { value: true, message: "Email is required." },
                pattern: { value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: 'Please enter a valid email'}
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.email && errors.email?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="password"
              name="password"
              id="password"
              autoComplete="new-password"
              placeholder="Password"
              {...register("password", {
                required: { value: true, message: "Password is required." },
                minLength: { value: 6, message: "Password cannot be less than 6 characters"},
                maxLength: { value: 30, message: "Password cannot be more than 30 characters"}
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.password && errors.password?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="password"
              name="confirmPassword"
              id="confirmPassword"
              autoComplete="new-password"
              placeholder="Confirm Password"
              {...register("confirmPassword", {
                required: {
                  value: true,
                  message: "confirmPassword is required.",
                },
                validate: (value, formValues) => {
                  if(value !== formValues.password) {
                    return 'Confirm password does not match the password';
                  }
                  return true;
                }
              })}
            />
            <div className={styles.validationError}>
              <span>
                {touchedFields.confirmPassword &&
                  errors.confirmPassword?.message}
              </span>
            </div>
          </div>
          <div className={styles.formGroup}>
                <button className={styles.submitButton} type="submit">Sign Up</button>
          </div>
          <p className={styles.text}>
            <span>
              Already have an account?
            </span>
          <Link className={styles.link} to="/login">Login</Link>
          </p>
        </form>
      </div>
    </div>
  );
}

export default Signup;

Это простая регистрационная форма, содержащая имя, имя пользователя, адрес электронной почты, пароль и поля для ввода пароля, с помощью которых пользователь может зарегистрироваться в нашем приложении.

Примечание. Здесь я использую библиотеку React Hook Form для управления формой регистрации. Вы можете использовать проверку формы HTML для проверки формы, если вы не хотите использовать какую-либо внешнюю библиотеку.

Signup.module.scss

Добавьте этот код в Signup.module.scss в папке Signup, чтобы оформить форму регистрации.

.container {
  display: flex;
  justify-content: center;
  background-color: rgb(0 128 128 / 10%);
  padding: 40px 20px;
  min-height: 100vh;
}

.formWrapper {
  display: flex;
  justify-content: center;
  box-sizing: border-box;
  align-items: center;
  width: 100%;
}

.form {
  padding: 20px 30px;
  width: 500px;
  background: #fff;
  box-shadow: 2px 2px 7px 2px rgb(0 0 0 / 20%);
  margin-right: 10px;

  @media (max-width: 768px) {
    width: 100%;
  }
}

.formTitle {
  color: teal;
  font-weight: 300;
  text-align: left;
  margin-bottom: 20px;
  font-size: 30px;

  @media (max-width: 768px) {
    font-size: 22px;
  }
}

.formGroup {
  position: relative;
  margin: 10px 0;
}

.input {
  font-size: 16px;
  padding: 11px 12px;
  width: 100%;
  outline: 1px solid #d4d5d9;
  border: none;
  color: #282c3f;
  caret-color: teal;
  font-weight: 500;

  &:focus {
    outline: 1px solid teal;
  }

  @media (max-width: 768px) {
    font-size: 14px;
  }

  @media (max-width: 768px) {
    font-size: 14px;
  }
}

.submitButton {
  background: teal;
  color: white;
  border: 1px solid transparent;
  padding: 10px 20px;
  font-size: 14px;
  text-transform: uppercase;
  cursor: pointer;
}

.validationError {
  color: red;
  height: 20px;
}

.text {
  color: #1b2839;
  font-weight: 500;

  @media (max-width: 768px) {
    font-size: 14px;
  }

  @media (max-width: 768px) {
    font-size: 14px;
  }
}

.link {
  color: teal;
  margin-left: 5px;
}

Добавление маршрута регистрации

Замените код внутри файла App.js кодом ниже:

import { createBrowserRouter, RouterProvider } from "react-router-dom";

import Signup from "./components/Signup/Signup";

function App() {
  const router = createBrowserRouter([
    {
      path: "/",
      element: <h1>Hello World!!</h1>,
    },
    {
      path: "sign-up",
      element: <Signup />,
    },
  ]);

  return (
    <div className="App">
      <RouterProvider router={router} />
    </div>
  );
}

export default App;

Теперь вы можете посетить страницу регистрации, перейдя по ссылке http://localhost:3000/sign-up.

Как заставить форму работать

Итак, мы создали форму регистрации, но пользователи не могут зарегистрироваться через нее. Давайте свяжем форму с серверной частью, что позволит пользователям зарегистрироваться, если они еще этого не сделали.

Сохранение состояния аутентификации с помощью Context API

Прежде чем мы свяжем нашу форму с серверной частью, давайте создадим контекст для хранения текущего аутентифицированного пользователя.

auth-context.js

Добавьте этот файл в каталог src/contexts и добавьте в этот файл следующий код.

import * as React from 'react';
import PropTypes from 'prop-types';

import { STATUS } from '../utils/utils';

const initialState = {
  user: {},
  token: null,
  expiresAt: null,
  isAuthenticated: false,
  status: STATUS.PENDING,
};

const AuthContext = React.createContext({
  ...initialState,
  login: (user = {}, token = '', expiresAt = '') => {},
  logout: () => {},
  updateUser: () => {},
  setAuthenticationStatus: () => {},
});

const authReducer = (state, action) => {
  switch (action.type) {
    case 'login': {
      return {
        user: action.payload.user,
        token: action.payload.token,
        expiresAt: action.payload.expiresAt,
        isAuthenticated: true,
        verifyingToken: false,
        status: STATUS.SUCCEEDED,
      };
    }
    case 'logout': {
      return {
        ...initialState,
        status: STATUS.IDLE,
      };
    }
    case 'updateUser': {
      return {
        ...state,
        user: action.payload.user,
      };
    }
    case 'status': {
      return {
        ...state,
        status: action.payload.status,
      };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
};

const AuthProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(authReducer, initialState);

  const login = React.useCallback((user, token, expiresAt) => {
    dispatch({
      type: 'login',
      payload: {
        user,
        token,
        expiresAt,
      },
    });
  }, []);

  const logout = React.useCallback(() => {
    dispatch({
      type: 'logout',
    });
  }, []);
  
  const updateUser = React.useCallback((user) => {
    dispatch({
      type: 'updateUser',
      payload: {
        user,
      },
    });
  }, []);

  const setAuthenticationStatus = React.useCallback((status) => {
    dispatch({
      type: 'status',
      payload: {
        status,
      },
    });
  }, []);

  const value = React.useMemo(
    () => ({ ...state, login, logout, updateUser, setAuthenticationStatus }),
    [state, setAuthenticationStatus, login, logout, updateUser]
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

const useAuth = () => {
  const context = React.useContext(AuthContext);

  if (context === undefined) {
    throw new Error('useAuth must be used within a AuthProvider');
  }

  return context;
};

AuthProvider.propTypes = {
  children: PropTypes.element.isRequired,
};

export { AuthProvider, useAuth };

В этом коде я использую пользовательский хук useAuth, который возвращает текущее значение контекста, созданное с использованием AuthContext, который содержит состояние аутентификации вместе с функциями, используемыми для входа в систему, выхода из системы и обновления статуса аутентификации.

Я также определяю компонент AuthProvider, который возвращает поставщика контекста AuthContext.Provider. Компонент AuthProvider использует хук useReducer для обновления состояния аутентификации всякий раз, когда пользователь входит или выходит из приложения.

Состояние, возвращаемое useReducer, затем передается как значение (вместе с такими функциями, как login и logout, которые обновляют состояние аутентификации с использованием метода отправки, возвращаемого хуком useReducer) в AuthContext.Provider. AuthContext.Provider также содержит компоненты, переданные компоненту AuthProvider в качестве дочерних элементов.

utils.js

Добавьте этот код в файл src/utils/utils.js.

const STATUS = Object.freeze({
  IDLE: 'idle',
  PENDING: 'pending',
  SUCCEEDED: 'succeeded',
  FAILED: 'failed',
});

export {
    STATUS
}

Этот код определяет объект, который используется для представления состояния HTTP-запроса, будь то ожидание, сбой или успешное выполнение.

Использование AuthProvider

Замените код внутри файла src/index.js кодом ниже:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { AuthProvider } from './contexts/auth-context';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <AuthProvider>
      <App />
    </AuthProvider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Используя этот код, каждый компонент, вложенный глубоко внутри компонента <App />, теперь может получить доступ и обновить состояние аутентификации с помощью пользовательского хука useAuth.

Подключение формы к серверной части

Обновите код внутри файла Signup.js следующим кодом:

import { Link, useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import axios from "axios";

import { useAuth } from "../../contexts/auth-context";
import { STATUS } from "../../utils/utils";

import styles from "./Signup.module.scss";

const Signup = () => {

  const {
    handleSubmit,
    register,
    formState: { errors, touchedFields },
  } = useForm({
    defaultValues: {
      name: "",
      username: "",
      email: "",
      password: "",
      confirmPassword: "",
    },
    mode: "onChange",
  });

  const navigate = useNavigate();

  const { login, setAuthenticationStatus } = useAuth();

  const onSubmit = async (values) => {
    const newUser = {
      name: values.name,
      username: values.username,
      email: values.email,
      password: values.password,
      confirmPassword: values.confirmPassword,
    };

    try {
      setAuthenticationStatus(STATUS.PENDING);
      const response = await axios.post("/api/auth/sign-up", newUser);
      setAuthenticationStatus(STATUS.SUCCEEDED);
      const { user, token, expiresAt } = response.data;
      login(user, token, expiresAt);
      navigate("/");
    } catch (error) {
      alert(error.response.data.error.message);
      setAuthenticationStatus(STATUS.FAILED);
    }
  };

  return (
    <div className={styles.container}>
      <div className={styles.formWrapper}>
        <form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
          <h1 className={styles.formTitle}>Create New Account</h1>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="text"
              name="name"
              id="name"
              placeholder="Name"
              {...register("name", {
                required: { value: true, message: "Name is required." },
                minLength: {
                  value: 2,
                  message: "Name cannot be less than 2 characters",
                },
                maxLength: {
                  value: 30,
                  message: "Name cannot be more than 30 characters",
                },
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.name && errors.name?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="text"
              name="username"
              id="username"
              placeholder="Username"
              {...register("username", {
                required: { value: true, message: "Username is required." },
                minLength: {
                  value: 2,
                  message: "Username cannot be less than 2 characters",
                },
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.username && errors.username?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="email"
              name="email"
              id="email"
              autoComplete="email"
              placeholder="Email"
              {...register("email", {
                required: { value: true, message: "Email is required." },
                pattern: {
                  value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
                  message: "Please enter a valid email",
                },
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.email && errors.email?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="password"
              name="password"
              id="password"
              autoComplete="new-password"
              placeholder="Password"
              {...register("password", {
                required: { value: true, message: "Password is required." },
                minLength: {
                  value: 6,
                  message: "Password cannot be less than 6 characters",
                },
                maxLength: {
                  value: 30,
                  message: "Password cannot be more than 30 characters",
                },
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.password && errors.password?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="password"
              name="confirmPassword"
              id="confirmPassword"
              autoComplete="new-password"
              placeholder="Confirm Password"
              {...register("confirmPassword", {
                required: {
                  value: true,
                  message: "confirmPassword is required.",
                },
                validate: (value, formValues) => {
                  if (value !== formValues.password) {
                    return "Confirm password does not match the password";
                  }
                  return true;
                },
              })}
            />
            <div className={styles.validationError}>
              <span>
                {touchedFields.confirmPassword &&
                  errors.confirmPassword?.message}
              </span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <button className={styles.submitButton} type="submit">
              Sign Up
            </button>
          </div>
          <p className={styles.text}>
            <span>Already have an account?</span>
            <Link className={styles.link} to="/login">
              Login
            </Link>
          </p>
        </form>
      </div>
    </div>
  );
};

export default Signup;

Метод onSubmit вызывается, когда пользователь отправляет форму регистрации. Внутри функции создается новый объект с именем newUser, свойства которого заданы для формирования значений. Внутри блока try...catch мы вызываем API регистрации с объектом newUser в качестве тела запроса.

Если запрос выполнен успешно, мы извлекаем пользователя, токен и время истечения срока действия токена из ответа и вызываем функцию входа в систему из хука useAuth. Эта функция обновляет состояние аутентификации с помощью нового пользователя и токена и перенаправляет пользователя на домашнюю страницу.

Компонент входа

Login.js

Добавьте этот код в файл Login.js в каталоге src/components/Login.

import { Link, useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import axios from "axios";

import { useAuth } from "../../contexts/auth-context";
import { STATUS } from "../../utils/utils";

import styles from "./Login.module.scss";

const Login = () => {
  const {
    handleSubmit,
    register,
    formState: { errors, touchedFields },
  } = useForm({
    defaultValues: {
      username: "",
      password: "",
    },
    mode: "onChange",
  });

  const navigate = useNavigate();

  const { login, setAuthenticationStatus } = useAuth();

  const onSubmit = async (values) => {
    const user = {
      username: values.username,
      password: values.password,
    };

    try {
      setAuthenticationStatus(STATUS.PENDING);
      const response = await axios.post("/api/auth/login", user);
      setAuthenticationStatus(STATUS.SUCCEEDED);
      const { user: userObj, token, expiresAt } = response.data;
      login(userObj, token, expiresAt);
      navigate('/');
    } catch (error) {
      alert(error.response.data.error.message);
    }
  };

  return (
    <div className={styles.container}>
      <div className={styles.formWrapper}>
        <form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
          <h1 className={styles.formTitle}>Sign In</h1>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="text"
              name="username"
              id="username"
              aria-label="Username or Email"
              required
              placeholder="Username or Email"
              {...register("username", {
                required: { value: true, message: "This field is required." },
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.name && errors.name?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <input
              className={styles.input}
              type="password"
              name="password"
              id="password"
              required
              placeholder="Password"
              {...register("password", {
                required: { value: true, message: "Password is required." },
              })}
            />
            <div className={styles.validationError}>
              <span>{touchedFields.password && errors.password?.message}</span>
            </div>
          </div>
          <div className={styles.formGroup}>
            <button className={styles.submitButton} type="submit">
              Sign In
            </button>
          </div>
          <p className={styles.text}>
            <span>Don't have an account?</span>
            <Link className={styles.link} to="/sign-up">
              Sign Up
            </Link>
          </p>
        </form>
      </div>
    </div>
  );
};

export default Login;

Login.module.scss

Добавьте этот код в файл Login.module.scss в каталоге src/components/Login.

.container {
  min-height: 100vh;
  background-color: rgb(0 128 128 / 10%);
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 40px 20px;
}

.formWrapper {
  max-width: 500px;
  width: 100%;
}

.form {
  background-color: #fff;
  box-shadow: 2px 2px 7px 2px rgb(0 0 0 / 20%);
  width: 100%;
  padding: 20px 30px;
}

.formTitle {
  color: teal;
  font-weight: 300;
  text-align: left;
  margin-bottom: 20px;
  font-size: 30px;
}

.formGroup {
  position: relative;
  margin: 10px 0;
}

.input {
  font-size: 16px;
  padding: 11px 12px;
  width: 100%;
  outline: 1px solid #d4d5d9;
  border: none;
  color: #282c3f;
  caret-color: teal;
  font-weight: 500;

  &:focus {
    outline: 1px solid teal;
  }

  @media (max-width: 768px) {
    font-size: 14px;
  }

  @media (max-width: 768px) {
    font-size: 14px;
  }
}

.submitButton {
  background: teal;
  color: white;
  border: 1px solid transparent;
  padding: 10px 20px;
  font-size: 14px;
  text-transform: uppercase;
  cursor: pointer;

  :disabled {
    background-color: grey;
  }
}

.validationError {
  color: red;
  height: 20px;
}

.text {
  color: #1b2839;
  font-weight: 500;

  @media (max-width: 768px) {
    font-size: 14px;
  }

  @media (max-width: 768px) {
    font-size: 14px;
  }
}

.link {
  color: teal;
  margin-left: 5px;
}

Добавление маршрута входа

Замените код внутри файла App.js кодом ниже:

import { createBrowserRouter, RouterProvider } from "react-router-dom";

import Signup from "./components/Signup/Signup";
import Login from './components/Login/Login';

function App() {
  const router = createBrowserRouter([
    {
      path: "/",
      element: <h1>Hello World!!</h1>,
    },
    {
      path: "sign-up",
      element: <Signup />,
    },
    {
    path: "login",
    element: (
        <Login />
    ),
  },
  ]);

  return (
    <div className="App">
      <RouterProvider router={router} />
    </div>
  );
}

export default App;

Теперь вы можете посетить страницу входа, перейдя на http://localhost:3000/login

Добавление автоматической аутентификации

Поскольку мы знаем, что токен доступа действителен только в течение 15 минут, без автоматической аутентификации пользователю придется снова входить в систему каждый раз, когда истечет срок действия токена. Чтобы пользователи не выходили из системы каждый раз, когда истечет срок действия токена, давайте реализуем автоматическую аутентификацию:

Что такое тихая аутентификация?

Тихая аутентификация — это процесс обновления токена доступа пользователя в фоновом режиме до истечения срока действия токена или когда пользователь перезагружает страницу. Поскольку мы не храним токены доступа в локальном хранилище (для предотвращения XSS-атак), мы также должны выполнять автоматическую аутентификацию при перезагрузке страницы.

Замените код внутри App.js кодом ниже:

import { useCallback, useEffect } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import axios from "axios";

import Signup from "./components/Signup/Signup";
import Login from "./components/Login/Login";

import { useAuth } from "./contexts/auth-context";

function App() {
  // New Code
  const { login, logout, isAuthenticated, expiresAt } = useAuth();

  const refreshAccessToken = useCallback(async () => {
    try {
      const response = await axios.post(
        "/api/auth/refresh",
        {},
        {
          withCredentials: true,
        }
      );

      const { user, accessToken, expiresAt } = response.data;

      if (response.status === 204) {
        logout();
      } else {
        login(user, accessToken, expiresAt);
      }
    } catch (error) {
      logout();
    }
  }, [login, logout]);

  useEffect(() => {
    refreshAccessToken();
  }, [refreshAccessToken]);

  useEffect(() => {
    let refreshAccessTokenTimerId;

    if (isAuthenticated) {
      refreshAccessTokenTimerId = setTimeout(() => {
        refreshAccessToken();
      }, new Date(expiresAt).getTime() - Date.now() - 10 * 1000);
    }

    return () => {
      if (isAuthenticated && refreshAccessTokenTimerId) {
        clearTimeout(refreshAccessTokenTimerId);
      }
    };
  }, [expiresAt, isAuthenticated, refreshAccessToken]);

  // Existing code 
  const router = createBrowserRouter([
    {
      path: "/",
      element: <h1>Hello World!!</h1>,
    },
    {
      path: "sign-up",
      element: <Signup />,
    },
    {
      path: "login",
      element: <Login />,
    },
  ]);

  return (
    <div className="App">
      <RouterProvider router={router} />
    </div>
  );
}

export default App;

Итак, что здесь происходит?

Давайте разберем код и шаг за шагом объясним его функциональность:

  • refreshAccessToken функция: эта функция отвечает за обновление токена доступа. Для этого он делает запрос POST к конечной точке /api/auth/refresh. Поскольку токен обновления хранится в виде файла cookie httpOnly, я также устанавливаю для параметра withCredentials значение true, чтобы автоматически отправлять токен обновления вместе с запросом. Если запрос успешен, мы вызываем функцию login для обновления токена доступа, хранящегося в памяти, в противном случае мы выходим из системы пользователя, если есть какая-либо ошибка.
  • Выполнение автоматической аутентификации при перезагрузке страницы. Поскольку токен доступа хранится только в памяти, а не в локальном хранилище или файле cookie, мы должны выполнять автоматическую аутентификацию всякий раз, когда перезагружается страница или приложение загружается в первый раз. время. Мы делаем это, вызывая функцию refreshAccessToken внутри первого хука useEffect, который запускается только при монтировании компонента App.
  • Выполнение автоматической аутентификации до истечения срока действия токена доступа. Второй useEffect отвечает за выполнение автоматической аутентификации до истечения срока действия токена доступа. Этот эффект запускается всякий раз, когда пользователь регистрируется или входит в приложение или когда обновляется токен доступа. Внутри этого эффекта мы сначала проверяем, аутентифицирован ли пользователь, и если да, то мы устанавливаем таймер с помощью setTimeout, который выполняет функцию refreshAccessToken() за 10 секунд до истечения срока действия токена доступа. Мы сбрасываем таймер при запуске очистки эффекта.

Добавление дополнительных маршрутов и компонентов

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

Добавление домашнего компонента

Добавьте этот код в файл Home.js в каталоге src/components/Home.

import { useAuth } from "../../contexts/auth-context";

import styles from "./Home.module.scss";

const Home = () => {
  const { user } = useAuth();

  return (
    <div className={styles.container}>
      <h1 className={styles.heading}><span className={styles.colorTeal}>Welcome</span> <span className={styles.colorBlack}>{user.name}</span></h1>
    </div>
  );
};

export default Home;

Это просто домашняя страница, на которой мы печатаем имя текущего аутентифицированного пользователя на экране.

Home.module.scss

Добавьте этот код в файл Home.module.scss в каталоге src/components/Home.

.container {
    margin-top: 120px;
    padding: 15px;
}

.heading {
    text-align: center;
}

.colorTeal {
    color: teal;
}

.colorBlack {
    color: #1b2839;
}

Добавление компонента пользователей

Добавьте этот код в файл Users.js в каталоге src/components/Users.

import { useEffect, useState } from "react";
import axios from "axios";

import User from "./User/User";

import { useAuth } from "../../contexts/auth-context";

import styles from './Users.module.scss';

const Users = () => {
  const { token } = useAuth();
  const [users, setUsers] = useState([]);

  useEffect(() => {
    axios
      .get("/api/users/list", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      })
      .then((res) => {
        setUsers(res.data.data);
      })
      .catch((error) => {
        console.log("Something went wrong.", error);
      });
  }, [token]);

  return (
    <div className={styles.container}>
      {users.map((user) => (
        <div key={user.id} className={styles.userContainer}>
            <User user={user}/>
        </div>
      ))}
    </div>
  );
};

export default Users;

В этом компоненте мы сначала получаем список пользователей с помощью HTTP-запроса GET к конечной точке /api/users/list, передавая токен доступа в качестве заголовка авторизации. Если запрос выполнен успешно, мы устанавливаем состояние users со списком пользователей, полученным с сервера.

Users.module.scss

Добавьте этот код в файл Users.module.scss в каталоге src/components/Users.

.container {
    margin-top: 150px;
}

.userContainer {
    margin-bottom: 20px;
}

Добавление пользовательского компонента

Добавьте этот код в файл User.js в каталоге src/components/Users/User.

import styles from './User.module.scss';

const User = ({ user }) => {
  return (
    <div className={styles.container}>
      <div className={styles.imageContainer}>
        <img
          className={styles.image}
          src="https://img.freepik.com/free-vector/businessman-character-avatar-isolated_24877-60111.jpg"
          alt="User Avatar"
        />
      </div>
      <div>
        <h2>{user.name}</h2>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
          ornare neque quis purus tempus interdum. Lorem ipsum dolor sit amet,
          consectetur adipiscing elit.{" "}
        </p>
      </div>
    </div>
  );
};

export default User;

User.module.scss

Добавьте этот код в файл User.module.scss в каталоге src/components/Users/User.

.container {
  display: flex;
  max-width: 600px;
  margin: 0 auto;
  gap: 20px;
  align-items: center;
  box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2);
  padding: 20px 18px;
}

.imageContainer {
  width: 200px;
  height: 100px;
  overflow: hidden;
  flex-basis: 100px;
  flex-shrink: 0;
  flex-grow: 0;
  border-radius: 50%;
}

.image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

Добавление маршрутов для компонентов Home и Users

Обновите конфигурацию маршрутизатора в App.js новой конфигурацией.

import { useCallback, useEffect } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import axios from "axios";

import Signup from "./components/Signup/Signup";
import Login from "./components/Login/Login";
import Home from "./components/Home/Home";
import Users from "./components/Users/Users";

import { useAuth } from "./contexts/auth-context";

function App() {
  const { login, logout, isAuthenticated, expiresAt } = useAuth();

  const refreshAccessToken = useCallback(async () => {
    try {
      const response = await axios.post(
        "/api/auth/refresh",
        {},
        {
          withCredentials: true,
        }
      );

      const { user, accessToken, expiresAt } = response.data;

      if (response.status === 204) {
        logout();
      } else {
        login(user, accessToken, expiresAt);
      }
    } catch (error) {
      logout();
    }
  }, [login, logout]);

  useEffect(() => {
    refreshAccessToken();
  }, [refreshAccessToken]);

  useEffect(() => {
    let refreshAccessTokenTimerId;

    if (isAuthenticated) {
      refreshAccessTokenTimerId = setTimeout(() => {
        refreshAccessToken();
      }, new Date(expiresAt).getTime() - Date.now() - 10 * 1000);
    }

    return () => {
      if (isAuthenticated && refreshAccessTokenTimerId) {
        clearTimeout(refreshAccessTokenTimerId);
      }
    };
  }, [expiresAt, isAuthenticated, refreshAccessToken]);
  
// New code
  const router = createBrowserRouter([
    {
      path: "/",
      element: <Home />,
    },
    {
      path: "sign-up",
      element: <Signup />,
    },
    {
      path: "login",
      element: <Login />,
    },
    {
      path: "users",
      element: <Users />
    }
  ]);

// Existing code
  return (
    <div className="App">
      <RouterProvider router={router} />
    </div>
  );
}

export default App;

Теперь вы можете посетить домашнюю страницу и страницу пользователей, перейдя по http://localhost:3000 и http://localhost:3000/users.

Главная страница

Страница пользователей

Добавление панели навигации

Давайте добавим компонент Navbar, который отображает ссылки для входа и регистрации, когда пользователь не вошел в систему, и отображает кнопку выхода, когда пользователь вошел в систему.

Navbar.js

Добавьте этот код внутрь Navbar.js в каталоге src/components/Navbar.

import { Link, useNavigate } from "react-router-dom";
import axios from "axios";

import { useAuth } from "../../contexts/auth-context";

import styles from "./Navbar.module.scss";

const Navbar = () => {
  const { isAuthenticated, token, logout } = useAuth();
  const navigate = useNavigate();

  const logOutHandler = async () => {
    try {
      await axios.post(
        "/api/auth/logout",
        {},
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      );
      logout();
      navigate('/login');
    } catch (error) {
      console.log("Something went wrong.", error);
    }
  };

  return (
    <header className={styles.header}>
      <nav className={styles.navigation}>
        <div>
          <Link className={styles.brand} to="/">
            Demo App
          </Link>
        </div>
        <div className={styles.navigationListContainer}>
          <ul className={styles.navigationList}>
            {!isAuthenticated && (
              <>
                <li className={styles.navigationItem}>
                  <Link className={styles.navigationLink} to="/login">
                    Login
                  </Link>
                </li>
                <li className={styles.navigationItem}>
                  <Link className={styles.navigationLink} to="/sign-up">
                    Sign Up
                  </Link>
                </li>
              </>
            )}
            {isAuthenticated && (
              <>
                <li className={styles.navigationItem}>
                  <Link className={styles.navigationLink} to="/users">
                    Users
                  </Link>
                </li>
                <li className={styles.navigationItem}>
                  <button
                    className={styles.navigationLink}
                    onClick={logOutHandler}
                  >
                    Log out
                  </button>
                </li>
              </>
            )}
          </ul>
        </div>
      </nav>
    </header>
  );
};

export default Navbar;

Здесь происходит несколько вещей, позвольте мне объяснить их шаг за шагом:

  • Поскольку мы используем пользовательский хук useAuth() для аутентификации пользователей, он предоставляет нам набор свойств и функций для эффективного управления процессом аутентификации.
  • Свойство isAuthenticated используется для проверки подлинности пользователя. Если пользователь аутентифицирован, мы показываем кнопку выхода из системы; в противном случае мы отображаем кнопки входа и регистрации.
  • logOutHandler вызывается, когда пользователь нажимает кнопку выхода из системы. Он отправляет запрос POST на конечную точку /api/auth/logout с токеном доступа в качестве заголовка авторизации и вызывает функцию logout(), как только мы получаем успешный ответ от сервера.
  • logout() отвечает за очистку пользовательского объекта от состояния редуктора.
  • После выхода пользователя из системы мы перенаправляем его на страницу входа.

Navbar.module.scss

Добавьте этот код в файл Navbar.module.scss в каталоге src/components/Navbar.

.header {
    height: 70px;
    width: 100%;
    background-color: white;
    display: flex;
    align-items: center;
    padding: 0 20px;
    -webkit-box-shadow: 0px 8px 5px 0px rgba(0, 0, 0, 0.05);
    -moz-box-shadow: 0px 8px 5px 0px rgba(0, 0, 0, 0.05);
    box-shadow: 0px 8px 5px 0px rgba(0, 0, 0, 0.05);
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;

    @media (max-width: 768px) {
        height: 60px;
    }
}

.navigation {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex: 1;
}

.brand {
    color: #1b2839;
    text-decoration: none;
    font-size: 24px;
    font-weight: 700;
    text-transform: uppercase;

    &:hover {
        color: teal;
    }

    @media (max-width: 768px) {
        font-size: 22px;
    }
}

.navigationList {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    list-style: none;
    flex-basis: 33%;

    @media (max-width: 768px) {
        flex-direction: column;
    }
}

.navigationItem {
    margin-right: 20px;
    transition: all 5s;

    @media (max-width: 768px) {
        margin-right: 0;
        margin-bottom: 30px;
    }
}

.navigationLink {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: #1b2839;
    cursor: pointer;
    text-transform: uppercase;
    text-decoration: none;
    font-size: 14px;

    &:hover {
        color: teal;
    }

    &:hover svg {
        stroke: teal;
    }

    &:hover>span {
        color: teal;
    }

    // button styles
    background: none;
    border: none;

    @media (max-width: 768px) {
        flex-direction: row;

        & svg {
            margin-right: 5px;
        }
    }
}

Добавление панели навигации в приложение

У нас есть два варианта добавления компонента Navbar в наше приложение. Во-первых, мы можем добавить его отдельно к каждому компоненту, например, к входу, регистрации или домашней странице. В качестве альтернативы мы можем использовать маршрут и компонент Layout, что позволяет нам сгруппировать несколько маршрутов вместе в общем макете.

Добавление маршрута и компонента Layout

Layout.js

Добавьте этот код в файл Layout.js в каталоге src/components/Layout.

import { Outlet } from "react-router-dom";

import Navbar from "../Navbar/Navbar";

const Layout = () => {
  return (
    <div>
      <Navbar />
      <div>
        <Outlet />
      </div>
    </div>
  );
};

export default Layout;

Здесь мы используем компонент <Outlet /> React Router для рендеринга дочерних компонентов маршрута внутри компонента Layout.

(Необязательно) Добавление защиты маршрута

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

Обновите код App.js следующим кодом:

import { useCallback, useEffect } from "react";
import {
  createBrowserRouter,
  RouterProvider,
  useLocation,
  Navigate,
} from "react-router-dom";
import PropTypes from "prop-types";
import axios from "axios";

import Signup from "./components/Signup/Signup";
import Login from "./components/Login/Login";
import Home from "./components/Home/Home";
import Users from "./components/Users/Users";
import Layout from "./components/Layout/Layout";
import SplashScreen from "./components/SplashScreen/SplashScreen";

import { useAuth } from "./contexts/auth-context";
import { STATUS } from "./utils/utils";

function App() {
  const { login, logout, isAuthenticated, expiresAt } = useAuth();

  const refreshAccessToken = useCallback(async () => {
    try {
      const response = await axios.post(
        "/api/auth/refresh",
        {},
        {
          withCredentials: true,
        }
      );

      const { user, accessToken, expiresAt } = response.data;

      if (response.status === 204) {
        logout();
      } else {
        login(user, accessToken, expiresAt);
      }
    } catch (error) {
      logout();
    }
  }, [login, logout]);

  useEffect(() => {
    refreshAccessToken();
  }, [refreshAccessToken]);

  useEffect(() => {
    let refreshAccessTokenTimerId;

    if (isAuthenticated) {
      refreshAccessTokenTimerId = setTimeout(() => {
        refreshAccessToken();
      }, new Date(expiresAt).getTime() - Date.now() - 10 * 1000);
    }

    return () => {
      if (isAuthenticated && refreshAccessTokenTimerId) {
        clearTimeout(refreshAccessTokenTimerId);
      }
    };
  }, [expiresAt, isAuthenticated, refreshAccessToken]);
  
// New code
  const router = createBrowserRouter([
    {
      element: <Layout />,
      children: [
        {
          path: "/",
          element: (
            <RequireAuth redirectTo="/sign-up">
              <Home />
            </RequireAuth>
          ),
        },
        {
          path: "sign-up",
          element: (
            <RedirectIfLoggedIn redirectTo="/">
              <Signup />
            </RedirectIfLoggedIn>
          ),
        },
        {
          path: "login",
          element: (
            <RedirectIfLoggedIn redirectTo="/">
              <Login />
            </RedirectIfLoggedIn>
          ),
        },
        {
          path: "users",
          element: (
            <RequireAuth redirectTo="/sign-up">
              <Users />
            </RequireAuth>
          ),
        }
      ],
    },
  ]);

  return (
    <div className="App">
      <RouterProvider router={router} />
    </div>
  );
}

// New code
const RequireAuth = ({ children, redirectTo }) => {
  const { isAuthenticated, status } = useAuth();
  const location = useLocation();

  if (status === STATUS.PENDING) return <SplashScreen />;

  return isAuthenticated ? (
    children
  ) : (
    <Navigate to={redirectTo} state={{ from: location }} />
  );
};

// New code
const RedirectIfLoggedIn = ({ children, redirectTo }) => {
  const { isAuthenticated, status } = useAuth();
  const location = useLocation();

  if (status === STATUS.PENDING) return <SplashScreen />;

  return isAuthenticated ? (
    <Navigate to={location.state?.from?.pathname || redirectTo} />
  ) : (
    children
  );
};

RequireAuth.propTypes = {
  children: PropTypes.element.isRequired,
  redirectTo: PropTypes.string.isRequired,
};

RedirectIfLoggedIn.propTypes = {
  children: PropTypes.element.isRequired,
  redirectTo: PropTypes.string.isRequired,
};

export default App;

Мы добавили два новых компонента в файл App.js 1. Компонент <RequireAuth /> и 2. <RedirectIfLoggedIn />. Компонент RequireAuth используется для защиты маршрутов, которые может посещать только аутентифицированный пользователь, а компонент RedirectIfLoggedIn используется для скрытия таких маршрутов, как регистрация и вход в систему, которые аутентифицированный пользователь не может посетить.

RequireAuth Компонент:

Этот компонент принимает два реквизита: children и redirectTo. Свойство redirectTo указывает путь, на который следует перенаправить пользователя, не прошедшего проверку подлинности. Он опирается на свойства isAuthenticated и status, возвращаемые хуком useAuth, для определения статуса аутентификации пользователя.

Если пользователь не аутентифицирован, компонент перенаправляет его на путь, указанный в свойстве redirectTo. С другой стороны, если пользователь аутентифицирован, компонент отображает дочерние компоненты, переданные через свойство children. Если статус проверки подлинности ожидается, компонент отображает компонент SplashScreen.

RedirectIfLoggedIn Компонент:

Этот компонент также принимает два реквизита: children и redirectTo. Свойство redirectTo указывает путь, на который должен быть перенаправлен аутентифицированный пользователь. Он опирается на свойства isAuthenticated и status, возвращаемые хуком useAuth, для определения статуса аутентификации пользователя.

Если пользователь аутентифицирован, компонент перенаправляет его по пути, указанному в реквизите redirectTo. С другой стороны, если пользователь не аутентифицирован, компонент отображает дочерние компоненты, переданные через свойство children. Если статус проверки подлинности ожидается, компонент отображает компонент SplashScreen.

SplashScreen Компонент:

Добавьте этот код в файл SplashScreen.js в каталоге src/components/SplashScreen.

import styles from './SplashScreen.module.scss';

const SplashScreen = () => {
  return (
    <div className={styles.container}>
      <div className={styles.iconContainer}>
        <div>
            ...loading
        </div>
      </div>
    </div>
  );
};

export default SplashScreen;

SplashScreen.module.scss

Добавьте этот код в файл SplashScreen.module.scss в каталоге src/components/SplashScreen.

.container {
    background-color: #fff;
    color: teal;
    position: fixed;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    z-index: 999;
}

.iconContainer {
    position: absolute;
    top: 50%;
    left: 50%;
    translate: (-50%, -50%);
    color: teal;
}

Конечный результат

Исходный код

Вы можете скачать исходный код из моего репозитория GitHub:



Заключение

Итак, мы узнали, как реализовать аутентификацию на основе токенов обновления и доступа в React и NodeJS. Мы узнали разницу между токенами доступа и обновления, а также узнали, как выполнять автоматическую аутентификацию, обновляя токены доступа в фоновом режиме. Мы также узнали, как защитить маршруты в приложениях React с помощью React Router.

Вот и все, и надеемся, что эта статья оказалась вам полезной! Пожалуйста, не стесняйтесь оставлять комментарии ниже и спрашивать что-либо, предлагать отзывы или просто общаться. Вы также можете подписаться на меня в Hashnode, Medium и Twitter. Ваше здоровье! ⭐️