NextJs приложение с Express API – Аутентификация с помощью cookies

Вопрос или проблема

У меня есть приложение Next.js (14 – роутер приложения), которое работает с внешним API на Express.

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

Проблема в том, что мои куки хранятся в браузере. Я создал fetchWrapper, который могу запускать как на сервере, так и на клиенте; если это на клиенте, он автоматически пересылает куки. Однако, если это на сервере, он берет куки и устанавливает их в заголовок. Это работает, но у меня есть несоответствие с куками.

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

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

Есть ли у вас какие-либо рекомендации по управлению аутентификацией между Next.js (сервером и клиентом) и моим API на Express?

let cookies;
if (typeof window === 'undefined') {
  cookies = require('next/headers').cookies;
}

type FetchOptions = {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  body?: any;
};

const defaultHeaders = {
  'Content-Type': 'application/json',
};

async function regenerateToken(): Promise<void> {
    const url = `/auth/refresh`;
    const response = fetchWrapper<{ token: string }>(url, {
        method: 'POST',
  }, true);
  return response;
}

function parseCookies(cookieString) {
  if(!cookieString) return []

  const result = [];
  const cookies = cookieString.split(/,(?=\s*\w+=)/);

  cookies.forEach(cookie => {
    const [nameValue] = cookie.split(';');
    const [name, value] = nameValue.trim().split('=');
    result.push({ name, value });
  });

  return result
}
const cookiesToSend = {}

async function fetchWrapper<T>(url: string, options: FetchOptions = {}, retry: boolean = false, req = null): Promise<T> {

  const { method = 'GET', headers = {}, body } = options;
  if (!url.startsWith(process.env.NEXT_PUBLIC_API_URL)) {
    url = process.env.NEXT_PUBLIC_API_URL + url;
  }

  if(!process.browser){
    if(!retry){
      const cookieStore = await cookies()
      cookiesToSend.accessToken = cookieStore.get('accessToken')
      cookiesToSend.refreshToken = cookieStore.get('refreshToken')
    }

    defaultHeaders.Cookie=""
    for(let c in cookiesToSend){
      if(cookiesToSend[c] == undefined) continue
      defaultHeaders.Cookie += `${cookiesToSend[c]?.name}=${cookiesToSend[c]?.value};`
    }
  }

  try {
    const response = await fetch(url, {
      method,
      credentials: 'include',
      headers: { ...defaultHeaders, ...headers },
      body: body ? JSON.stringify(body) : undefined,
      cache: 'no-store'
    });

    if(!process.browser){
      const cookiesHeader = response.headers.get('set-cookie')
      const parsedCookies = parseCookies(cookiesHeader);
      parsedCookies.forEach(cookie => {
        cookiesToSend[cookie.name] = { name: cookie.name, value: cookie.value };
      });
    }

    if (!response.ok) {
        const errorMessage = await response.json();
        const error = {
            status: response.status,
            message: errorMessage || 'Произошла ошибка',
        };

        if (error.status === 401 && errorMessage.error_code == "TOKEN_EXPIRED") {
            if(!retry){
                    const responseRegenerate = await regenerateToken();
                    return fetchWrapper(url, options, true);
            }else{
                    if (typeof window !== 'undefined'){
                        window.location.href="https://stackoverflow.com/connexion";
                        return Promise.reject(new Error('Перенаправление на страницу входа...'));
                    } 
            }

        }
      throw error;
    }

    return response.json();
  } catch (error) {
    throw error;
    console.log(error);

  }
}

export default fetchWrapper;

Ответ или решение

Ваша задача по интеграции аутентификации с использованием http-only cookies между приложением Next.js и внешним API Express представляет собой интересный вызов. Давайте разберем, как вы можете управлять аутентификацией, обеспечивая при этом корректную работу с сессиями, токенами и cookies как на сервере, так и на клиенте.

Поскольку вы используете http-only cookies для хранения токенов доступа и обновления, эти cookies недоступны для JavaScript на стороне клиента, что обеспечивает дополнительный уровень безопасности. Однако это также означает, что проверка существования и содержание этих cookies на стороне клиента требует определенных механизмов.

2. Параметры конфигурации вашего fetchWrapper

Ваш текущий подход к обработке заголовков и cookies в fetchWrapper имеет некоторые проблемные моменты. Часто возникает ошибка интерпретации cookies и их актуализация после обновления токена. Подумайте над следующим:

А. Структура вашего fetchWrapper

Убедитесь, что вы правильно учитываете состояние токенов на обоих уровнях (сервисе и клиенте). Кроме того, логика повторных попыток при истечении срока действия токена должна управляться очень аккуратно.

Пример улучшенного фрагмента:
async function fetchWrapper<T>(url: string, options: FetchOptions = {}, retry: boolean = false): Promise<T> {

  const { method = 'GET', headers = {}, body } = options;

  if (!url.startsWith(process.env.NEXT_PUBLIC_API_URL)) {
    url = process.env.NEXT_PUBLIC_API_URL + url;
  }

  if (!process.browser) {
    const cookieStore = await cookies(); // Получение cookies на серверной стороне
    headers.Cookie = `${cookieStore.get('accessToken')}; ${cookieStore.get('refreshToken')};`;
  }

  try {
    const response = await fetch(url, {
      method,
      credentials: 'include',
      headers: { ...defaultHeaders, ...headers },
      body: body ? JSON.stringify(body) : undefined,
    });

    if (response.status === 401 && !retry) {
      await regenerateToken(); // Попытка обновления токена
      return fetchWrapper(url, options, true); // Повторный запрос с новым токеном
    }

    if (!response.ok) {
        throw new Error('Что-то пошло не так');
    }

    return await response.json();
  } catch (error) {
    console.error(error);
    throw error; // Бросаем ошибку для обработки в вызывающем коде
  }
}

3. Обновление токена

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

  • Создайте отдельный метод для обновления токена, который не только получает новый токен, но и обновляет cookies на серверной стороне.
  • Опционально, реализуйте логику, которая позволяет сбрасывать сессии.

4. Предзагрузка данных

С учётом использования Next.js и его серверных возможностей, важно предзагрузить данные во время рендеринга страницы:

export async function getServerSideProps(context) {
  const { req } = context;
  const cookies = req.headers.cookie || '';

  // Получаем данные, используя `fetchWrapper` с необходимыми cookies
  const data = await fetchWrapper(`${process.env.NEXT_PUBLIC_API_URL}/data`, { headers: { Cookie: cookies } });

  return { props: { data } }; // Передаем данные в компонент
}

5. Подсказки по аутентификации

  • Отслеживайте состояние токена и его наличие в cookies. Подумайте о внедрении хука useEffect для обновления состояния клиентского приложения при изменении токена.
  • Убедитесь, что все запросы к API имеют возможность обработки ошибок и возможного повторного запроса при истечении токена.
  • Применяйте централизованное хранилище (например, Redux или React Context) для управления состоянием аутентификации.

Заключение

Управление аутентификацией в приложении Next.js с использованием http-only cookies и Express API требует внимательного подхода к обработке токенов и cookies. Применение предложенных рекомендаций должно помочь вам в решении существующих проблем и более эффективной интеграции аутентификации в ваше приложение. Ключом будет корректная настройка fetchWrapper, эффективное обновление токенов и использование серверных функций Next.js для предзагрузки данных без задержек.

Оцените материал
Добавить комментарий

Капча загружается...