← Назад к вопросам
Как работает Refresh Token?
1.8 Middle🔥 162 комментариев
#Браузер и сетевые технологии
Комментарии (2)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Refresh Token: механизм и реализация
Refresh Token — это специальный токен, который используется для получения нового Access Token без повторного ввода учётных данных. Это ключевой компонент безопасной системы аутентификации.
Почему нужны два токена
Основная идея: использовать два токена с разным временем жизни:
// Access Token:
// - Живёт 15-30 минут
// - Используется для доступа к защищённым ресурсам
// - Если украдён, вред ограничен коротким временем
// - Содержит информацию о пользователе (payload)
// Refresh Token:
// - Живёт 7-30 дней (или даже больше)
// - Используется ТОЛЬКО для получения нового Access Token
// - Хранится более безопасно (например, в secure cookies)
// - Если украдён, можно отозвать (revoke) на сервере
Как работает процесс
Шаг 1: Первоначальная аутентификация
// Пользователь отправляет учётные данные
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
// Сервер вернул оба токена:
// {
// "accessToken": "eyJhbGc...",
// "refreshToken": "dXNlcjox...",
// "expiresIn": 900
// }
return data;
};
Шаг 2: Использование Access Token
// Клиент использует accessToken для запросов
const fetchUserProfile = async (accessToken: string) => {
const response = await fetch('/api/me', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (response.status === 401) {
// Token истёк, нужно его обновить
return null;
}
return response.json();
};
Шаг 3: Обновление токена (Refresh)
// Когда Access Token истекает, используем Refresh Token
const refreshAccessToken = async (refreshToken: string) => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken })
});
if (!response.ok) {
// Refresh Token невалидный или истёк
// Нужно заново авторизоваться
redirectToLogin();
return null;
}
const data = await response.json();
// { "accessToken": "eyJhbGc..." }
localStorage.setItem('accessToken', data.accessToken);
return data.accessToken;
};
Реализация с перехватом запросов (Interceptor)
В real-world приложении нужна автоматизация обновления токена:
// lib/api.ts
import axios, { AxiosInstance } from 'axios';
const api: AxiosInstance = axios.create({
baseURL: 'https://api.example.com'
});
let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');
let isRefreshing = false;
let failedQueue = [];
const processQueue = (token: string | null) => {
failedQueue.forEach(prom => {
if (token) {
prom.resolve(token);
} else {
prom.reject(new Error('Refresh failed'));
}
});
failedQueue = [];
};
api.interceptors.request.use(
(config) => {
if (accessToken && config.headers) {
config.headers.Authorization = 'Bearer ' + accessToken;
}
return config;
},
(error) => Promise.reject(error)
);
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (!isRefreshing) {
isRefreshing = true;
originalRequest._retry = true;
try {
const response = await axios.post(
'https://api.example.com/auth/refresh',
{ refreshToken }
);
accessToken = response.data.accessToken;
localStorage.setItem('accessToken', accessToken);
originalRequest.headers.Authorization = 'Bearer ' + accessToken;
processQueue(accessToken);
isRefreshing = false;
return api(originalRequest);
} catch (refreshError) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
processQueue(null);
isRefreshing = false;
window.location.href = '/login';
return Promise.reject(refreshError);
}
} else {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers.Authorization = 'Bearer ' + token;
return api(originalRequest);
});
}
}
return Promise.reject(error);
}
);
export default api;
Где хранить токены
1. LocalStorage
// Плюсы: просто реализовать, доступен длительное время
// Минусы: уязвим для XSS (кросс-сайтовый скрипт)
localStorage.setItem('accessToken', token);
// Пример XSS атаки:
// скрипт может украсть токен
2. SessionStorage
// Плюсы: удаляется при закрытии вкладки
// Минусы: уязвим для XSS, теряется при обновлении
sessionStorage.setItem('accessToken', token);
3. Memory переменная
// Плюсы: невозможно украсть XSS-ом
// Минусы: теряется при обновлении страницы
let accessToken: string | null = null;
window.addEventListener('load', async () => {
const newToken = await refreshAccessToken();
accessToken = newToken;
});
4. Secure HTTP-Only Cookie (РЕКОМЕНДУЕТСЯ)
// Плюсы: невозможно украсть XSS-ом, автоматически отправляется
// Минусы: требует корректной настройки CORS и SameSite
// Сервер отправляет header:
Set-Cookie: refreshToken=dXNlcjox; HttpOnly; Secure; SameSite=Strict
// Браузер автоматически отправляет cookie
fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
Полный Flow с React Context
// contexts/AuthContext.tsx
import React, { createContext, useState, useEffect } from 'react';
import api from '@/lib/api';
interface AuthContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
isAuthenticated: boolean;
}
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const initAuth = async () => {
try {
const response = await api.post('/auth/refresh');
localStorage.setItem('accessToken', response.data.accessToken);
const userResponse = await api.get('/me');
setUser(userResponse.data);
} catch (error) {
setUser(null);
} finally {
setIsLoading(false);
}
};
initAuth();
}, []);
const login = async (email: string, password: string) => {
const response = await api.post('/auth/login', { email, password });
localStorage.setItem('accessToken', response.data.accessToken);
const userResponse = await api.get('/me');
setUser(userResponse.data);
};
const logout = async () => {
await api.post('/auth/logout');
localStorage.removeItem('accessToken');
setUser(null);
};
return (
<AuthContext.Provider value={{
user, isLoading, login, logout,
isAuthenticated: !!user
}}>
{children}
</AuthContext.Provider>
);
}
Best Practices
- Используй HTTPS — токены чувствительны к перехвату
- Короткое время жизни Access Token (15-30 мин) — снижает ущерб при краже
- Длинное время жизни Refresh Token (7-30 дней) — удобство для пользователя
- Secure cookies для Refresh Token — максимальная безопасность
- Возможность отозвать токены — удалить токен на сервере при logout
- Ротация Refresh Token — выдавай новый при обновлении access token
- Логирование подозрительной активности — обновления с разных IP
Ловушки и ошибки
// Плохо: хранить оба токена в localStorage
localStorage.setItem('refreshToken', token);
// Украденный refresh token работает 30 дней
// Хорошо: refresh token в secure cookie, access token в памяти
// Украденный access token работает только 15 минут
// Плохо: не проверять срок токена перед запросом
fetch('/api/data', {
headers: { 'Authorization': 'Bearer oldExpiredToken' }
});
// Хорошо: проактивно обновить если осталось меньше минуты
if (tokenExpiresIn < 60) {
await refreshAccessToken();
}
Итог
Refresh Token — фундаментальный паттерн современной аутентификации:
- Access Token — короткоживущий, для доступа к ресурсам
- Refresh Token — долгоживущий, для получения новых access токенов
- Secure cookies — лучший способ хранения refresh tokens
- Interceptors — автоматизируют процесс обновления
Правильная реализация делает систему безопасной и удобной одновременно.