Введение
Прежде чем мы сможем иметь дело с 401-неавторизованным токеном аутентификации, нам нужно понять, что это такое.
Если пользователь использует токен доступа с истекшим сроком действия, мы просто возвращаем ошибку 401: Несанкционированный запрос! пользователю.
Это помогает нам гарантировать, что сгенерированные токены доступа не могут быть использованы не по назначению, поэтому, даже если хакер получит доступ к токену доступа, он не сможет использовать его долго.
Мы не хотим, чтобы нашим пользователям приходилось постоянно вводить свои учетные данные на странице входа, чтобы оставаться на связи.
Вот почему нам нужно время от времени обновлять токен доступа пользователя, чтобы обеспечить очень плавный пользовательский интерфейс.
Redux Saga — это библиотека промежуточного программного обеспечения для Redux, которая позволяет вам управлять побочными эффектами в вашем магазине Redux, такими как асинхронные действия, такие как запросы API.
При обработке кода состояния HTTP 401 с Redux Saga это может дать несколько преимуществ:
- Перехват ответов 401: с помощью Redux Saga вы можете легко перехватывать ответы HTTP 401 из ваших запросов API. Это позволяет вам обрабатывать ошибки аутентификации централизованно, вместо того, чтобы обрабатывать их отдельно в каждом компоненте или модуле.
- Отправка действий: Redux Saga упрощает отправку действий при возникновении ошибки 401. Например, вы можете отправить действие, чтобы перенаправить пользователя на страницу входа или отобразить сообщение об ошибке.
- Обработка ошибок: с помощью Redux Saga вы можете обрабатывать ошибки более структурированным образом, что упрощает отладку и устранение неполадок. Например, вы можете перехватывать ошибки, используя блоки try-catch, и отправлять действия при ошибках, чтобы обновить ваш магазин или отобразить значимое сообщение об ошибке для пользователя.
- Координация нескольких действий: Redux Saga позволяет координировать несколько действий, упрощая управление сложными асинхронными рабочими процессами. Например, вы можете использовать Redux Saga для отправки действия по обновлению токена доступа, когда срок его действия истекает, а затем повторить неудачный запрос API с новым токеном.
Прежде чем приступить к чтению этой статьи, вы должны ознакомиться с
токен доступа и токен обновления,
Эта статья направлена на решение проблемы с кодом состояния 401 в Redux-Saga и предоставление решений для ее преодоления.
Основная проблема с кодом состояния 401 — это чрезмерные запросы, отправленные на маршруты, что приводит к необходимости повторных запросов.
Чтобы решить эту проблему, мы предлагаем изучить возможность отправки запросов без повторных попыток, тем самым сократив количество запросов и обеспечив более плавный пользовательский интерфейс.
Давайте зададим 2 вопроса, что такое возможно, и решим их, не отправляя много запросов API.
Вопрос:
Предположим, вы сделали запрос POST API и получили ошибку токена с истекшим сроком действия (код состояния 401), указывающую на то, что срок действия токена доступа истек.
В таком случае, какие шаги вы бы предприняли, чтобы справиться с этой ситуацией?
function* onSubmit(values) { try { const apiRequestValues = { method: 'POST', url: `https://www.random-url.com/api/firstQuestion`, data: values } const result = yield apiRequestWrapper(apiRequestValues); return result; } catch (err) { console.error(err); } };
Ответ с использованием Redux-Saga:
Первым шагом нам нужно настроить Redux-Saga в нашем магазине Redux.
добавить в файл redux.js
import { configureStore } from '@reduxjs/toolkit' import createSagaMiddleware from 'redux-saga' import reducer from './reducers' import mySaga from './sagas' // create the saga middleware const sagaMiddleware = createSagaMiddleware() // mount it on the Store const store = configureStore( reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(sagaMiddleware), ) // then run the saga sagaMiddleware.run(mySaga)
Теперь, когда вы добавили промежуточное ПО Saga в свой магазин, пришло время создать функциональность промежуточного ПО.
создайте новый файл sagas.js
import { all, takeLatest } from 'redux-saga/effects' import { tokenMiddleware } from './sagaActions' import { GET_TOKEN_SAGA_MIDDLEWARE } from '../constants/constants' export default function* mySaga() { yield all([ yield takeLeading(GET_TOKEN_SAGA_MIDDLEWARE, tokenMiddleware), // Add new functions ]); }
В этом файле мы можем добавить функции к нашему промежуточному ПО, которые будут следить за типом действия.
Каждый раз, когда мы вызываем действие, эти функции будут выполняться.
В нашем примере мы используем эффект "takeLeading", который представляет собой эффект саги, который блокирует код до тех пор, пока ранее созданная сага не завершится, прежде чем снова прослушивать новый шаблон.
Другими словами, takeLeading прослушивает действия, но не запускает сагу каждый раз, когда она выполняется.
Вместо этого он относится только к последнему выполнению и игнорирует предыдущие экземпляры того же типа действия.
Таким образом, при каждом выполнении действия типа GET_TOKEN_SAGA_MIDDLEWARE, независимо от того, сколько раз оно выполняется, эффект takeLeading будет относиться только к первому выполнению.
Это означает, что действие tokenMiddleware будет выполнено только один раз.
Позже мы увидим, зачем это делать и что это для нас решает.
Вернемся к нашему первому ответу и создадим новый файл sagaActions.js.
import { call, put } from 'redux-saga/effects' import axiosInstance from '../configAPI/configAPI' import { setAccessToken, setRefreshToken } from './sagaSelectors' import { SET_TOKEN_SUCCESS, SET_TOKEN_FAIL } from '../constants/constants' export function* tokenMiddleware({ payload }) { try { const { data: { accessToken, refreshToken }, } = yield call(axiosInstance, { url: `https://www.random-url.com/getAccessToken`, method: 'post', headers: { 'Content-Type': 'application/json', }, data: { ...payload, // To enhance the functionality of the tokenMiddleware, include any additional data required to ensure that all requests are routed through it. }, }); yield put(setAccessToken(accessToken)); yield put(setRefreshToken(refreshToken)); yield put({ type: SET_TOKEN_SUCCESS }); } catch (e) { yield put({ type: SET_TOKEN_FAIL }); } }
Функция "tokenMiddleware" принимает объектный параметр "payload", который содержит любые данные, которые необходимо передать в конечную точку API.
Функция "tokenMiddleware" отправляет запрос POST на URL-маршрут "https://www.random-url.com/getAccessToken", чтобы получить "accessToken и refreshToken.
Если запрос выполнен успешно, действия setAccessToken и setRefreshToken отправляются с использованием эффекта put.
Кроме того, отправляется действие типа "SET_TOKEN_SUCCESS", указывающее, что был получен новый "accessToken".
Если запрос завершается ошибкой с кодом состояния 401 (неавторизованный токен аутентификации), отправляется действие "SET_TOKEN_FAIL", чтобы указать, что новый "accessToken" не может быть получен, поскольку Срок действия refreshToken истек.
Мы обсудим, как обрабатываются эти действия позже.
Давайте добавим в файл sagaActions.js новый метод
export function* apiRequestWrapper({ headers, ...rest }, retryCount: number = 0) { const { accessTokenWithJWTDecode, accessToken, refreshToken } = yield select(getUserConfig); if (accessTokenWithJWTDecode.exp > Date.now()) { throw new Error({ name: 'Access token expired', response: { status: 401, data: { detail: `This error is from the apiRequestWrapper because the access token is invalid`, }, } }); } try { const response = yield call(axiosInstance, { ...rest, headers: { ...headers, Authorization: `Bearer ${accessToken}`, }, }); return response; } catch (e) { const { response } = e; const status = response?.status; const isExpiredTokenError = status === 401; if (isExpiredTokenError) { yield put({ type: GET_TOKEN_SAGA_MIDDLEWARE, payload: { refresh_token: refreshToken } }); const { type } = yield take([SET_SUCCESS_TOKEN, SET_FAIL_TOKEN]); if (type === SET_SUCCESS_TOKEN) { return yield apiRequestWrapper({ headers, ...rest }); } if(type === SET_FAIL_TOKEN) { return yield put(doForceLogout()); } } if (retryCount) { return yield apiRequestWrapper({ headers, ...rest }, retryCount - 1); } throw e; } }
Функция "apiRequestWrapper" — это наша основная функция, которую мы будем использовать каждый раз, когда будем отправлять запрос API.
Для начала мы будем использовать эффект "select", который представляет собой метод, предоставляемый Redux-Saga, который позволяет нам извлекать значения из хранилища, аналогично "useSelector". хук в Redux. В частности, мы будем использовать селектор getUserConfig для извлечения из хранилища только данных userConfig.
Затем мы проверяем, не истек ли срок действия accessTokenWithJWTDecode.
Если это так, мы немедленно выдаем новую ошибку и полностью пропускаем блок try.
Ошибка будет обнаружена блоком catch, что позволит нам правильно обработать токен с истекшим сроком действия.
Внутри блока try мы используем эффект call из Redux-Saga.
Этот эффект блокирует, а это означает, что сага будет ждать разрешения обещания, прежде чем перейти к следующему шагу, подобно поведению "async/await".
Мы передаем accessToken в заголовке запроса API только после того, как удостоверимся, что срок его действия не истек, как было определено в ходе предыдущей проверки.
Затем результат запроса API извлекается и возвращается.
Самая интересная часть этого кода находится в блоке catch.
Во-первых, мы проверяем ответ об ошибке, если код состояния равен 401, мы можем приступить к настоящей цели этой статьи.
Для этого мы используем переменную "isExpiredTokenError", если она имеет значение true, мы отправляем действие типа "GET_TOKEN_SAGA_MIDDLEWARE" с помощью "put" эффект, который должен показаться вам знакомым по предыдущей части статьи.
Это действие имеет тот же тип, что и ПО промежуточного слоя, которое мы добавили для выполнения функции генератора "tokenMiddleware" в "sagas.js".
Мы также включаем полезную нагрузку, содержащую refreshToken.
Когда мы создали функцию-генератор mySaga и добавили промежуточное ПО типа GET_TOKEN_SAGA_MIDDLEWARE, мы обернули его takeLeading.
Это было сделано для того, чтобы гарантировать, что в этот момент, независимо от того, сколько запросов API мы отправим в "apiRequestWrapper", промежуточное ПО будет выполнять только функцию генератора "tokenMiddleware" один раз.
После выполнения промежуточного действия типа "GET_TOKEN_SAGA_MIDDLEWARE" мы используем эффект "take", чтобы дождаться отправки определенного действия.
Эффект "взять" создает описание эффекта, которое указывает промежуточному программному обеспечению ожидать указанного действия в хранилище.
Затем генератор приостанавливается до тех пор, пока не будет отправлено действие, соответствующее шаблону.
В нашем случае тип действия определяется ответом промежуточного ПО GET_TOKEN_SAGA_MIDDLEWARE.
После отправки указанного действия генератор возобновляет выполнение и передает полученные данные вместе с типом ожидаемого действия в функцию генератора apiRequestWrapper.
Функция генератора "tokenMiddleware" обрабатывает действия типа "SET_SUCCESS_TOKEN" и "SET_FAIL_TOKEN".
Ответ от функции определяет следующие шаги в "apiRequestWrapper".
Если ответ имеет тип SET_SUCCESS_TOKEN, это означает, что токен обновления действителен.
Затем функция будет рекурсивно вызывать себя, чтобы повторить исходный запрос API.
С другой стороны, если ответ имеет тип "SET_FAIL_TOKEN", это означает, что токен обновления недействителен, и пользователю необходимо принудительно выйти из системы.
С другой стороны, если мы не получили код состояния 401 и указали аргумент retryCount в apiRequestWrapper, функция повторит попытку использования исходного API. рекурсивно запрашивать число раз, указанное в аргументе retryCount.
В конце мы выбрасываем ошибку, которая была поймана в блоке try-catch, для дальнейшей обработки ошибки или регистрации.
В этой статье мы рассмотрели еще одну важную проблему, связанную с запросами API — проблему accessToken и refreshToken — и предложили решение.
Существенный вопрос:
Предположим, вы сделали запрос POST API, чтобы отправить публикацию, и вам нужно отправить 5 запросов API публикации или более с помощью Promise.all.
Однако вы получаете сообщение об ошибке маркера с истекшим сроком действия (код состояния 401), указывающее на то, что срок действия маркера доступа истек.
Предположим, что токен обновления действителен.
Как бы вы поступили в этой ситуации, не отправляя 5 или более запросов на обновление токена доступа?
function* onSubmit(values) { try { const apiRequestValues1 = { method: 'POST', url: `https://www.random-url.com/api/secondQuestion1`, data: values } const apiRequestValues2 = { method: 'POST', url: `https://www.random-url.com/api/secondQuestion2`, data: values } const apiRequestValues3 = { method: 'POST', url: `https://www.random-url.com/api/secondQuestion3`, data: values } const apiRequestValues4 = { method: 'POST', url: `https://www.random-url.com/api/secondQuestion4`, data: values } const apiRequestValues5 = { method: 'POST', url: `https://www.random-url.com/api/secondQuestion5`, data: values } const result = yield Promise.all([ apiRequestWrapper(apiRequestValues1), apiRequestWrapper(apiRequestValues2), apiRequestWrapper(apiRequestValues3), apiRequestWrapper(apiRequestValues4), apiRequestWrapper(apiRequestValues5), ]); return result; } catch (err) { console.error(err); } };
Ответ с использованием Redux-Saga:
Мы уже решили эту проблему, используя эффекты Redux-Saga. :-)
В заключение
Мы успешно решили проблемы с токенами доступа и обновления, используя эффекты Redux-Saga.
Мы настроили промежуточное ПО для решения проблемы отправки нескольких запросов API для токена доступа и узнали, как использовать Redux-Saga для улучшения взаимодействия с пользователем и повышения производительности.
Внедряя методы, описанные в этой статье, мы можем обеспечить более плавный и эффективный поток запросов API, что в конечном итоге приведет к лучшему взаимодействию с пользователем.
Не стесняйтесь обращаться ко мне напрямую в LinkedIn — Нажмите здесь.