Вопрос или проблема
У меня есть приложение 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 как на сервере, так и на клиенте.
1. Понимание работы с Cookie
Поскольку вы используете 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 для предзагрузки данных без задержек.