Токены используются для аутентификации и авторизации HTTP-запросов.
Наиболее распространённый сценарий — передача JWT-токена через заголовок
Authorization.
RTK Query предоставляет несколько механизмов для автоматического добавления токенов:
prepareHeadersbaseQueryНа практике почти все приложения используют именно
prepareHeaders, поскольку этот механизм встроен в
fetchBaseQuery и позволяет централизованно модифицировать
запросы.
Наиболее распространённая схема:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
}
}),
endpoints: () => ({})
})
В данном примере:
prepareHeadersФункция вызывается перед отправкой каждого HTTP-запроса.
Сигнатура:
prepareHeaders: (headers, api) => {}
Где:
headers
— экземпляр Headers.
А объект api содержит:
{
getState,
endpoint,
type,
forced,
extra
}
Пример:
prepareHeaders: (headers, api) => {
console.log(api.endpoint)
console.log(api.type)
return headers
}
getState позволяет получить текущее состояние Redux
Store.
Пример структуры состояния:
{
auth: {
token: 'jwt-token-value',
user: {
id: 1,
name: 'Alex'
}
}
}
Получение токена:
const token = getState().auth.token
Полный пример:
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
}
Формат:
Authorization: Bearer token_value
является стандартом OAuth 2.0 и JWT-аутентификации.
Сервер обычно ожидает именно такой формат:
headers.set('Authorization', `Bearer ${token}`)
Без префикса Bearer многие backend-системы отклоняют
запрос.
Часто вместе с токеном добавляются:
Пример:
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
headers.set('Accept', 'application/json')
headers.set('X-App-Version', '1.0.0')
headers.set('X-Timezone', Intl.DateTimeFormat().resolvedOptions().timeZone)
return headers
}
Перезаписывает значение:
headers.set('Authorization', 'Bearer token')
Добавляет ещё одно значение:
headers.append('X-Test', 'value')
Для токенов почти всегда используется именно set.
Иногда токен хранится не в Redux Store, а в
localStorage.
Пример:
prepareHeaders: (headers) => {
const token = localStorage.getItem('token')
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
}
Такой подход имеет несколько проблем:
Если токен изменился:
localStorage.setItem('token', 'new-token')
компоненты Redux не узнают об изменении автоматически.
В server-side rendering отсутствует объект window.
Следующий код приведёт к ошибке:
localStorage.getItem('token')
localStorage уязвим для XSS-атак.
Наиболее распространённая архитектура:
{
auth: {
accessToken,
refreshToken,
user
}
}
Slice:
import { createSlice } from '@reduxjs/toolkit'
const authSlice = createSlice({
name: 'auth',
initialState: {
accessToken: null
},
reducers: {
setCredentials: (state, action) => {
state.accessToken = action.payload.accessToken
},
logout: (state) => {
state.accessToken = null
}
}
})
export const {
setCredentials,
logout
} = authSlice.actions
export default authSlice.reducer
Пример:
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.accessToken
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
}
Иногда токен нужен только для определённых endpoints.
Пример:
prepareHeaders: (headers, { endpoint, getState }) => {
const token = getState().auth.token
const protectedEndpoints = [
'getProfile',
'updateProfile',
'createPost'
]
if (token && protectedEndpoints.includes(endpoint)) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
}
RTK Query передаёт тип запроса:
type
Значения:
querymutationПример:
prepareHeaders: (headers, { type, getState }) => {
const token = getState().auth.token
if (type === 'mutation' && token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
}
Некоторые backend-приложения используют HttpOnly cookies вместо Bearer Token.
В таком случае:
baseQuery: fetchBaseQuery({
baseUrl: '/api',
credentials: 'include'
})
Теперь браузер автоматически отправляет cookies.
Варианты:
Cookies не отправляются.
Cookies отправляются только внутри текущего домена.
Cookies всегда отправляются, включая cross-origin запросы.
Иногда backend требует оба варианта:
baseQuery: fetchBaseQuery({
baseUrl: '/api',
credentials: 'include',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
}
})
Одно из главных преимуществ RTK Query — отсутствие необходимости вручную писать:
headers: {
Authorization: ...
}
в каждом endpoint.
Плохой вариант:
getProfile: builder.query({
query: () => ({
url: '/profile',
headers: {
Authorization: 'Bearer token'
}
})
})
Правильная архитектура:
baseQuery: fetchBaseQuery({
prepareHeaders
})
Иногда prepareHeaders недостаточно.
Например:
Пример:
const baseQuery = fetchBaseQuery({
baseUrl: '/api'
})
const baseQueryWithAuth = async (args, api, extraOptions) => {
const token = api.getState().auth.token
let headers = {}
if (token) {
headers.Authorization = `Bearer ${token}`
}
const result = await baseQuery(
{
...args,
headers: {
...args.headers,
...headers
}
},
api,
extraOptions
)
return result
}
Использование:
export const api = createApi({
reducerPath: 'api',
baseQuery: baseQueryWithAuth,
endpoints: () => ({})
})
JWT access token обычно живёт:
После этого сервер начинает возвращать:
401 Unauthorized
Распространённая архитектура:
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
import { setCredentials, logout } from './authSlice'
const baseQuery = fetchBaseQuery({
baseUrl: '/api',
credentials: 'include',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.accessToken
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
}
})
const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions)
if (result.error && result.error.status === 401) {
const refreshResult = await baseQuery(
{
url: '/auth/refresh',
method: 'POST'
},
api,
extraOptions
)
if (refreshResult.data) {
api.dispatch(
setCredentials(refreshResult.data)
)
result = await baseQuery(
args,
api,
extraOptions
)
} else {
api.dispatch(logout())
}
}
return result
}
Проблема:
Это создаёт race condition.
Популярное решение — использование mutex.
Пример с async-mutex:
import { Mutex } from 'async-mutex'
const mutex = new Mutex()
Далее:
await mutex.waitForUnlock()
И только один запрос выполняет refresh.
Типичная production-архитектура:
RTK Query может работать не только с REST.
Пример:
const graphqlBaseQuery =
({ baseUrl }) =>
async ({ body }, api) => {
const token = api.getState().auth.token
const result = await fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify(body)
})
return {
data: await result.json()
}
}
Некоторые сервисы используют API Key вместо JWT.
Пример:
prepareHeaders: (headers) => {
headers.set('X-API-Key', 'secret-key')
return headers
}
Либо:
headers.set(
'Authorization',
'ApiKey secret-key'
)
Токены и API-ключи часто передаются через переменные окружения.
Пример:
headers.set(
'X-API-Key',
process.env.REACT_APP_API_KEY
)
В Vite:
import.meta.env.VITE_API_KEY
Неправильно:
prepareHeaders: (headers) => {
headers.set('Authorization', 'token')
}
Правильно:
prepareHeaders: (headers) => {
headers.set('Authorization', 'token')
return headers
}
Ошибка:
getState().token
Правильно:
getState().auth.token
Ошибка:
headers.set(
'Authorization',
`Bearer ${token}`
)
при:
token === null
В результате сервер получает:
Authorization: Bearer null
Правильная проверка:
if (token) {
headers.set(
'Authorization',
`Bearer ${token}`
)
}
Ошибка:
headers: headers
при кастомном baseQuery.
Можно случайно затереть существующие заголовки.
Правильный вариант:
headers: {
...args.headers,
Authorization: `Bearer ${token}`
}
export const api = createApi({
reducerPath: 'api',
baseQuery: baseQueryWithReauth,
tagTypes: [
'User',
'Post',
'Comment'
],
endpoints: () => ({})
})
Где:
baseQueryWithReauth автоматически добавляет токены