[Из песочницы] Token, refresh token и создание асинхронной обертки для REST-запроса
В данном туториале мы кратко разберем, как реализовываются REST-запросы к API, требующие, чтобы пользователь был авторизован, и создадим асинхронную «обертку» для запроса, которая будет проверять авторизацию и своевременно ее обновлять.
Данные для авторизации
Сделав REST-запрос к api, куда мы отправили логин и пароль, в ответ мы получаем json следующего формата (значения взяты рандомные и строки обычно длиннее):
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSld",
"refresh_token": "1eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgS",
"expires_in": 124234149563
}
Полей в ответе может быть больше, например еще «token_type», «expires_on» и т. д., но, для данной реализации, нам нужны только три поля, приведенные выше.
Давайте их рассмотрим подробнее:
- access_token — токен, который нам нужно будет отправлять в шапке каждого запроса, для получения данных в ответ
- refresh_token — токен, который нам нужно будет отправлять, для получения нового токена, когда истечет время жизни старого
- expires_in — время жизни токена в секундах
Получение токена
Теперь создадим функцию, которая будет получать json, описанный выше, и сохранять его.
Хранить данные для авторизации мы будем в sessionStorage или localStorage, в зависимости от наших нужд. В первом случае данные хранятся до тех пор, пока пользователь не завершит сеанс или не закроет браузер, во втором случае данные в браузере будут храниться неограниченное время, пока по каким-либо причинам localStorage не будет очищен.
Функция для сохранения токена в sessionStorage:
function saveToken(token) {
sessionStorage.setItem('tokenData', JSON.stringify(token));
}
Функция для получения токена:
function getTokenData(login, password) {
return fetch('api/auth', {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
login,
password,
}),
})
.then((res) => {
if (res.status === 200) {
const tokenData = res.json();
saveToken(JSON.stringify(tokenData)); // сохраняем полученный токен в sessionStorage, с помощью функции, заданной ранее
return Promise.resolve()
}
return Promise.reject();
});
}
Таким образом мы получили токен с полями «access_token», «refresh_token» и «expires_in» и сохранили его в sessionStorage для дальнейшего использования.
Обновление токена
Токен полученный нами ранее имеет ограниченное время жизни, которое задано в поле «expires_in». После того как его время жизни истечет, пользователь не сможет получить новые данные, отправляя данный токен в запросе, поэтому нужно получить новый токен.
Получить токен мы можем двумя способами: первый способ это заново авторизовавшись, отправив логин и пароль на сервер. Но это нам не подходит, т. к. заставлять пользователя каждый раз заново вводить данные авторизации по истечению какого-то отрезка времени — неправильно, это надо делать автоматически. Но хранить где-то в памяти пару логин/пароль для автоматической отправки небезопасно, именно для этого и нужен «refresh_token», который был получен ранее вместе с «access_token» и хранится в sessionStorage. Отправив данный токен на другой адрес, который предоставляет api, мы сможем получить в ответ новый «свежий» токен.
Функция для обновления токена
function refreshToken(token) {
return fetch('api/auth/refreshToken', {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token,
}),
})
.then((res) => {
if (res.status === 200) {
const tokenData = res.json();
saveToken(JSON.stringify(tokenData)); // сохраняем полученный обновленный токен в sessionStorage, с помощью функции, заданной ранее
return Promise.resolve();
}
return Promise.reject();
});
}
С помощью кода выше мы перезаписали токен в sessionStorage и теперь по новой можем отправлять запросы к api.
Создание функции-обертки
Теперь создадим функцию, которая будет добавлять данные для авторизации в шапку запроса, и при необходимости их автоматически обновлять перед совершением запроса.
Так как в случае, если срок жизни токена истек, нам надо будет делать запрос нового токена, то наша функция будет асинхронной. Для этого мы будем использовать конструкцию async/await.
Функция-обертка
export async function fetchWithAuth(url, options) {
const loginUrl = '/login'; // url страницы для авторизации
let tokenData = null; // объявляем локальную переменную tokenData
if (sessionStorage.authToken) { // если в sessionStorage присутствует tokenData, то берем её
tokenData = JSON.parse(localStorage.tokenData);
} else {
return window.location.replace(loginUrl); // если токен отсутствует, то перенаправляем пользователя на страницу авторизации
}
if (!options.headers) { // если в запросе отсутствует headers, то задаем их
options.headers = {};
}
if (tokenData) {
if (Date.now() >= tokenData.expires_on * 1000) { // проверяем не истек ли срок жизни токена
try {
const newToken = await refreshToken(tokenData.refresh_token); // если истек, то обновляем токен с помощью refresh_token
saveToken(newToken);
} catch () { // если тут что-то пошло не так, то перенаправляем пользователя на страницу авторизации
return window.location.replace(loginUrl);
}
}
options.headers.Authorization = `Bearer ${tokenData.token}`; // добавляем токен в headers запроса
}
return fetch(url, options); // возвращаем изначальную функцию, но уже с валидным токеном в headers
}
С помощью кода выше мы создали функцию, которая будет добавлять токен к запросам в api. На эту функцию мы можем заменить fetch в нужных нам запросах, где требуется авторизация и для этого нам не потребуется менять синтаксис или добавлять в аргументы еще какие-либо данные.
Просто достаточно будет «импортнуть» ее в файл и заменить на нее стандартный fetch.
import fetchWithAuth from './api';
function getData() {
return fetchWithAuth('api/data', options)
}