Фото: FlyD на Unsplash
Вчора ввечері я поглиблено займався рефакторингом, зосередившись на покращенні способу обробки токенів доступу та оновлення в моєму застосунку. Керування токенами — це одна з тих областей у розробці програмного забезпечення, яка здається простим на перший погляд, але може швидко стати заплутаною, коли база коду зростає.
У цьому пості я пройду через весь процес спрощення обробки токенів, покращення прозорості та зменшення зайвого коду в базі.
Початкова точка:
Спочатку найпростішим підходом було зберігання токенів доступу та оновлення безпосередньо в localStorage. Цей метод добре працював для базових потреб аутентифікації, і токен доступу автоматично додавався до API запитів за допомогою перехоплювачів axios.
Ось як виглядала початкова реалізація:
import axios from 'axios';// Налаштування екземпляру axios
const api = axios.create({
baseURL: 'https://api.example.com',
});
// Додавання токену доступу до всіх запитів
api.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (error) => {
return Promise.reject(error);
});
Цей перехоплювач забезпечував автоматичне включення токену доступу з localStorage у кожен запит, спрощуючи шар сервісів API.
Однак, цей підхід не враховував перевірки терміну дії токену. Замість того, щоб заздалегідь перевірити, чи не вичерпався термін дії токену, я просто дозволяв запиту зазнати невдачі з відповіддю 401 Unauthorized
і запускати процес оновлення токену.
Проблема:
Хоча цей метод працював, він вводив повторення коду і складність. Кожен виклик API потребував обробки потенційної відповіді 401
, змушуючи мене вручну оновлювати токен і повторно надсилати запит в кількох місцях.
Початкова логіка виглядала ось так:
async function fetchData() {
try {
return await api.get('/data');
} catch (error) {
if (error.response?.status === 401) {
const refreshed = await refreshAccessToken();
if (refreshed) {
return api.get('/data');
}
}
throw error;
}
}
async function refreshAccessToken() {
const refreshToken = localStorage.getItem('refreshToken');
try {
const response = await axios.post('/auth/refresh', { token: refreshToken });
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
return true;
} catch {
return false;
}
}
Цей шаблон повторювався в кількох функціях API, що робило код:
- Складним для підтримки
- Помилковим через неузгоджене оброблення оновлення
- Зайвим, оскільки одна і та ж логіка перевірки
401
з'являлася в кількох місцях
Рефакторинг:
Щоб спростити це, я централізував логіку оновлення за допомогою перехоплювачів axios для обробки відповідей.
Замість того, щоб вручну оновлювати токени в кожній функції, перехоплювач автоматично обробляв би помилки 401 Unauthorized
.
Ось більш чисте рішення:
// Перехоплювач відповіді для обробки помилок 401
api.interceptors.response.use(
(response) => response, // Пропускаємо успішні відповіді
async (error) => {
if (error.response?.status === 401) {
try {
const res = await axios.post('/auth/refresh');
const { accessToken } = res.data;
// Зберігаємо новий токен та повторюємо невдалий запит
localStorage.setItem('accessToken', accessToken);
error.config.headers.Authorization = `Bearer ${accessToken}`;
return api(error.config); // Повторюємо оригінальний запит
} catch (refreshError) {
// Вихід з системи або редирект, якщо оновлення не вдалося
console.error('Токен оновлення вичерпано або є недійсним');
throw refreshError;
}
}
throw error;
}
);
Наступні кроки: Токени доступу з коротким терміном дії та HttpOnly токени для оновлення
Хоча цей рефакторинг покращив загальну структуру коду, зберігання токенів в localStorage все ще несе ризики безпеки (наприклад, вразливості XSS).
Щоб зменшити ці ризики, я впровадив наступні зміни:
- Токени доступу з коротким терміном дії — Токени доступу тепер діють лише 5 хвилин. Це обмежує можливості для потенційних атак.
- Управління станом в React — Токен доступу тепер зберігається в стані React (в пам'яті), що знижує вразливість до атак XSS.
- HttpOnly токени для оновлення — Токен для оновлення тепер зберігається як HttpOnly cookie. Це перешкоджає доступу до нього з боку JavaScript на клієнті, що підвищує безпеку.
Ось новий процес:
// Зберігаємо токен доступу в стані React
const [accessToken, setAccessToken] = useState(null);
useEffect(() => {
// Отримуємо новий токен доступу, якщо користувач увійшов або оновив сторінку
axios.post('/auth/refresh')
.then(res => {
setAccessToken(res.data.accessToken);
})
.catch(() => {
// Обробляємо вихід або редирект
});
}, []);
// Додаємо токен з стану до запитів
api.interceptors.request.use((config) => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
}, (error) => {
return Promise.reject(error);
});
Чому це працює:
- Токен доступу в пам'яті — Токени, що зберігаються в пам'яті, недоступні для скриптів, що захищає від атак XSS.
- HttpOnly токен для оновлення — Сервер автоматично надсилає новий токен доступу, якщо cookie для оновлення дійсне, що дозволяє обробку оновлення повністю на сервері.
- Безшовний досвід для користувача — Користувачі залишаються авторизованими, поки токен оновлення залишатиметься дійсним (наприклад, 7 днів), а токени доступу безшумно оновлюються кожні 5 хвилин.
Остання реалізація:
З цією новою конфігурацією, виклики API стали більш безпечними та ефективними:
async function fetchUserData() {
return api.get('/user');
}
async function fetchOrders() {
return api.get('/orders');
}
Результат — простішою та безпечнішою системою керування токенами, яка використовує стан React для короткотривалих токенів та HttpOnly cookie для довготривалих оновлень.
Основні висновки:
- Токени з коротким терміном дії знижують ризики — Швидке закінчення терміну дії токенів мінімізує загрози безпеці.
- HttpOnly cookies покращують безпеку — Зберігання токенів для оновлення на стороні сервера захищає від атак XSS.
- Пам'ятове зберігання токенів для доступу — Зберігання токенів в стані React замість localStorage допомагає уникнути вразливостей.
Цей рефакторинг зробив код більш безпечним, стійким і легким у підтримці. Якщо ви стикалися з керуванням токенами у своїх проектах, я буду радий почути, як ви підходили до цього. Поділіться своїм досвідом у коментарях!
Перекладено з: Refactoring Token Management: A Cleaner Approach to Handling Access and Refresh Tokens