Рефакторинг керування токенами: чистіший підхід до обробки токенів доступу та оновлення

pic

Фото: 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

Leave a Reply

Your email address will not be published. Required fields are marked *