← Назад к вопросам

Как работает 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

  1. Используй HTTPS — токены чувствительны к перехвату
  2. Короткое время жизни Access Token (15-30 мин) — снижает ущерб при краже
  3. Длинное время жизни Refresh Token (7-30 дней) — удобство для пользователя
  4. Secure cookies для Refresh Token — максимальная безопасность
  5. Возможность отозвать токены — удалить токен на сервере при logout
  6. Ротация Refresh Token — выдавай новый при обновлении access token
  7. Логирование подозрительной активности — обновления с разных 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 — автоматизируют процесс обновления

Правильная реализация делает систему безопасной и удобной одновременно.

Как работает Refresh Token? | PrepBro